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

deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new 4e9caba7c02 HIVE-30127: Iceberg: Upgrade iceberg version to 1.12.0 
(#6856)
4e9caba7c02 is described below

commit 4e9caba7c02a947a39c60de427acb7189c8c1fe8
Author: Raghav Aggarwal <[email protected]>
AuthorDate: Tue Oct 13 14:08:48 2026 +0530

    HIVE-30127: Iceberg: Upgrade iceberg version to 1.12.0 (#6856)
---
 .../iceberg/hive/HMSTablePropertyHelper.java       |   8 +-
 .../java/org/apache/iceberg/hive/HiveCatalog.java  |  56 ++++-
 .../apache/iceberg/hive/HiveOperationsBase.java    |   7 +-
 .../apache/iceberg/hive/HiveTableOperations.java   |  36 +--
 .../apache/iceberg/hive/HiveViewOperations.java    |  12 +-
 iceberg/iceberg-handler/pom.xml                    |   4 +
 .../apache/iceberg/mr/hive/IcebergAcidUtil.java    |   2 +-
 .../mr/hive/writer/HiveFileWriterFactory.java      | 140 +++++-------
 .../writer/HiveIcebergCopyOnWriteRecordWriter.java |   2 +-
 .../mr/hive/writer/HiveIcebergDeleteWriter.java    |   4 +-
 .../iceberg/mr/hive/writer/WriterBuilder.java      |   7 +-
 .../org/apache/iceberg/parquet/VariantUtil.java    |   8 +-
 .../java/org/apache/iceberg/data/FileHelpers.java  | 207 ++++++++++--------
 .../apache/iceberg/data/GenericAppenderHelper.java | 102 ++++++---
 .../iceberg/mr/hive/TestHiveIcebergCRUD.java       |  64 ++++--
 .../mr/hive/test/utils/HiveIcebergTestUtils.java   |  40 ++--
 .../src/main/java/org/apache/iceberg/DVUtil.java   | 214 ------------------
 .../apache/iceberg/deletes/BaseDVFileWriter.java   | 240 --------------------
 .../java/org/apache/iceberg/deletes/Deletes.java   | 242 ---------------------
 iceberg/pom.xml                                    |   2 +-
 itests/qtest-iceberg/pom.xml                       |   5 +
 pom.xml                                            |   9 +-
 .../org/apache/iceberg/rest/HMSCatalogAdapter.java |   8 +-
 .../rest/TestHMSCatalogAdapterSnapshotMode.java    | 176 +++++++++++++++
 standalone-metastore/pom.xml                       |   2 +-
 25 files changed, 597 insertions(+), 1000 deletions(-)

diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HMSTablePropertyHelper.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HMSTablePropertyHelper.java
index 0639e50debe..81840148cc7 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HMSTablePropertyHelper.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HMSTablePropertyHelper.java
@@ -122,7 +122,7 @@ public static void updateHmsTableForIcebergTable(
         obsoleteProps,
         currentLocation,
         parameters,
-        
BaseMetastoreTableOperations.ICEBERG_TABLE_TYPE_VALUE.toUpperCase(Locale.ENGLISH),
+        
BaseMetastoreTableOperations.ICEBERG_TABLE_TYPE_VALUE.toUpperCase(Locale.ROOT),
         metadata.schema(),
         maxHiveTablePropertySize);
     setStorageHandler(parameters, hiveEngineEnabled);
@@ -168,7 +168,7 @@ public static void updateHmsTableForIcebergView(
         obsoleteProps,
         currentLocation,
         parameters,
-        HiveOperationsBase.ICEBERG_VIEW_TYPE_VALUE.toUpperCase(Locale.ENGLISH),
+        HiveOperationsBase.ICEBERG_VIEW_TYPE_VALUE.toUpperCase(Locale.ROOT),
         metadata.schema(),
         maxHiveTablePropertySize);
     parameters.put(hive_metastoreConstants.META_TABLE_STORAGE, 
HIVE_ICEBERG_STORAGE_HANDLER);
@@ -335,8 +335,8 @@ static void verifyMetadataHash(TableMetadata metadata, 
String metadataHashFromHM
   }
 
   private static byte[] hashOf(TableMetadata tableMetadata) {
-    try (HashWriter hashWriter = new HashWriter("SHA-256", 
StandardCharsets.UTF_8)) {
-      JsonGenerator generator = JsonUtil.factory().createGenerator(hashWriter);
+    try (HashWriter hashWriter = new HashWriter("SHA-256", 
StandardCharsets.UTF_8);
+        JsonGenerator generator = 
JsonUtil.factory().createGenerator(hashWriter)) {
       TableMetadataParser.toJson(tableMetadata, generator);
       generator.flush();
       return hashWriter.getHash();
diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveCatalog.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveCatalog.java
index 95749370c82..123971e64aa 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveCatalog.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveCatalog.java
@@ -21,6 +21,7 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -36,6 +37,7 @@
 import org.apache.hadoop.hive.metastore.api.PrincipalType;
 import org.apache.hadoop.hive.metastore.api.Table;
 import org.apache.hadoop.hive.metastore.api.UnknownDBException;
+import org.apache.hadoop.hive.metastore.api.hive_metastoreConstants;
 import org.apache.iceberg.BaseMetastoreTableOperations;
 import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.CatalogUtil;
@@ -57,6 +59,7 @@
 import org.apache.iceberg.exceptions.NoSuchViewException;
 import org.apache.iceberg.exceptions.NotFoundException;
 import org.apache.iceberg.hadoop.HadoopFileIO;
+import org.apache.iceberg.io.CloseableGroup;
 import org.apache.iceberg.io.FileIO;
 import 
org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
 import org.apache.iceberg.relocated.com.google.common.base.MoreObjects;
@@ -108,6 +111,7 @@ public class HiveCatalog extends BaseMetastoreViewCatalog
   private boolean listAllTables = false;
   private boolean uniqueTableLocation;
   private Map<String, String> catalogProperties;
+  private CloseableGroup closeableGroup;
 
   public HiveCatalog() {
   }
@@ -157,6 +161,12 @@ public void initialize(String inputName, Map<String, 
String> properties) {
     }
 
     this.clients = new CachedClientPool(conf, properties);
+
+    this.closeableGroup = new CloseableGroup();
+    closeableGroup.addCloseable(fileIO);
+    closeableGroup.addCloseable(keyManagementClient);
+    closeableGroup.addCloseable(metricsReporter());
+    closeableGroup.setSuppressCloseFailure(true);
   }
 
   @Override
@@ -176,17 +186,18 @@ public List<TableIdentifier> listTables(Namespace 
namespace) {
     String database = namespace.level(0);
 
     try {
-      List<String> tableNames = clients.run(client -> 
client.getAllTables(database));
       List<TableIdentifier> tableIdentifiers;
 
       if (listAllTables) {
+        List<String> tableNames = clients.run(client -> 
client.getAllTables(database));
         tableIdentifiers = tableNames.stream()
             .map(t -> TableIdentifier.of(namespace, t))
             .collect(Collectors.toList());
       } else {
-        tableIdentifiers = listIcebergTables(
-                tableNames, namespace, 
BaseMetastoreTableOperations.ICEBERG_TABLE_TYPE_VALUE);
-
+        // Retrieving only the matching table names from HMS via server-side 
filter
+        tableIdentifiers =
+            listIcebergTablesByFilter(
+                namespace, 
BaseMetastoreTableOperations.ICEBERG_TABLE_TYPE_VALUE);
       }
 
       LOG.debug("Listing of namespace: {} resulted in the following tables: 
{}", namespace, tableIdentifiers);
@@ -352,6 +363,37 @@ private List<TableIdentifier> listIcebergTables(
         .collect(Collectors.toList());
   }
 
+  /**
+   * Lists the {@link TableIdentifier} of all Iceberg tables under the given 
database.
+   *
+   * @param namespace the namespace corresponding to the database
+   * @param tableTypeValue the value of {@code parameters.table_type} to 
filter on.
+   */
+  private List<TableIdentifier> listIcebergTablesByFilter(
+      Namespace namespace, String tableTypeValue) throws TException, 
InterruptedException {
+    String database = namespace.level(0);
+    // HMS normalizes the `table_type` parameter value to upper case when 
persisting it
+    // (e.g. "iceberg" is stored as "ICEBERG"), so the filter value must be 
upper-cased to match.
+    // Note: `like` (rather than `=`) is used intentionally. Some HMS backends 
(e.g. Derby and
+    // Oracle, where PARAM_VALUE is a CLOB) do not support `=` comparison on 
property values and
+    // fail the filter query (see HIVE-21614); newer HMS versions internally 
rewrite `=` to `like`
+    // for this reason. As the value carries no `%`/`_` wildcards, `like` is 
equivalent to `=` here.
+    String filter =
+        hive_metastoreConstants.HIVE_FILTER_FIELD_PARAMS +
+            BaseMetastoreTableOperations.TABLE_TYPE_PROP +
+            " like \"" +
+            tableTypeValue.toUpperCase(Locale.ROOT) +
+            "\"";
+
+    List<String> icebergTableNames =
+        clients.run(
+            client -> client.listTableNamesByFilter(database, filter, (short) 
-1 /* no limit */));
+
+    return icebergTableNames.stream()
+        .map(tableName -> TableIdentifier.of(namespace, tableName))
+        .collect(Collectors.toList());
+  }
+
   @SuppressWarnings("checkstyle:CyclomaticComplexity")
   private void renameTableOrView(
           TableIdentifier from,
@@ -830,10 +872,8 @@ public Map<String, String> properties() {
 
   @Override
   public void close() throws IOException {
-    super.close();
-
-    if (keyManagementClient != null) {
-      keyManagementClient.close();
+    if (closeableGroup != null) {
+      closeableGroup.close();
     }
   }
 
diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveOperationsBase.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveOperationsBase.java
index bf2684daa3f..4913a091a46 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveOperationsBase.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveOperationsBase.java
@@ -221,15 +221,16 @@ static void cleanupMetadataAndUnlock(
 
   default Table newHmsTable(String hmsTableOwner) {
     Preconditions.checkNotNull(hmsTableOwner, "'hmsOwner' parameter can't be 
null");
-    final long currentTimeMillis = System.currentTimeMillis();
+    // epoch seconds; HMS Thrift stores these as i32
+    final int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000L);
 
     Table newTable =
         new Table(
             table(),
             database(),
             hmsTableOwner,
-            (int) currentTimeMillis / 1000,
-            (int) currentTimeMillis / 1000,
+            currentTimeSeconds,
+            currentTimeSeconds,
             Integer.MAX_VALUE,
             null,
             Collections.emptyList(),
diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
index 6d3e3cc3635..c240463f68d 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
@@ -49,7 +49,6 @@
 import org.apache.iceberg.encryption.EncryptionUtil;
 import org.apache.iceberg.encryption.KeyManagementClient;
 import org.apache.iceberg.encryption.PlaintextEncryptionManager;
-import org.apache.iceberg.encryption.StandardEncryptionManager;
 import org.apache.iceberg.exceptions.AlreadyExistsException;
 import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.CommitStateUnknownException;
@@ -221,6 +220,7 @@ protected void doRefresh() {
                 .map(Lists::newLinkedList)
                 .orElseGet(Lists::newLinkedList);
 
+        // Active transactions may still need keys that are not in committed 
metadata.
         if (encryptionManager != null) {
           Set<String> keyIdsFromMetadata =
               
encryptedKeys.stream().map(EncryptedKey::keyId).collect(Collectors.toSet());
@@ -246,28 +246,14 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
     boolean newTable = base == null;
     encryptionPropsFromMetadata(metadata.properties());
     boolean keepHiveStats = conf.getBoolean(ConfigProperties.KEEP_HIVE_STATS, 
false);
-    final TableMetadata tableMetadata;
-    EncryptionManager encrManager = encryption();
-    if (encrManager instanceof StandardEncryptionManager) {
-      // Add new encryption keys to the metadata
-      TableMetadata.Builder builder = TableMetadata.buildFrom(metadata);
-      for (Map.Entry<String, EncryptedKey> entry : 
EncryptionUtil.encryptionKeys(encrManager).entrySet()) {
-        builder.addEncryptionKey(entry.getValue());
-      }
-
-      tableMetadata = builder.build();
-    } else {
-      tableMetadata = metadata;
-    }
-
-    boolean hiveEngineEnabled = hiveEngineEnabled(tableMetadata, conf);
-    String newMetadataLocation = writeNewMetadataIfRequired(newTable, 
tableMetadata);
+    String newMetadataLocation = writeNewMetadataIfRequired(newTable, 
metadata);
+    boolean hiveEngineEnabled = hiveEngineEnabled(metadata, conf);
 
     BaseMetastoreOperations.CommitStatus commitStatus =
         BaseMetastoreOperations.CommitStatus.FAILURE;
     boolean updateHiveTable = false;
 
-    HiveLock lock = lockObject(base != null ? base : tableMetadata);
+    HiveLock lock = lockObject(base != null ? base : metadata);
     try {
       lock.lock();
 
@@ -287,7 +273,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
         LOG.debug("Committing existing table: {}", fullName);
       } else {
         tbl = newHmsTable(
-            tableMetadata.property(HiveCatalog.HMS_TABLE_OWNER, 
HiveHadoopUtil.currentUser())
+            metadata.property(HiveCatalog.HMS_TABLE_OWNER, 
HiveHadoopUtil.currentUser())
         );
         LOG.debug("Committing new table: {}", fullName);
       }
@@ -304,8 +290,8 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
 
       tbl.setSd(
           HiveOperationsBase.storageDescriptor(
-              tableMetadata.schema(),
-              tableMetadata.location(),
+              metadata.schema(),
+              metadata.location(),
               hiveEngineEnabled, // set to pickup any schema changes
               bucketCols,
               numBuckets));
@@ -324,7 +310,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
       if (base != null) {
         removedProps =
             base.properties().keySet().stream()
-                .filter(key -> !tableMetadata.properties().containsKey(key))
+                .filter(key -> !metadata.properties().containsKey(key))
                 .collect(Collectors.toSet());
 
         Preconditions.checkArgument(
@@ -341,7 +327,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
       HMSTablePropertyHelper.updateHmsTableForIcebergTable(
           newMetadataLocation,
           tbl,
-          tableMetadata,
+          metadata,
           removedProps,
           hiveEngineEnabled,
           maxHiveTablePropertySize,
@@ -395,7 +381,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
           // issue for example, and triggers this exception. So we need 
double-check to make sure
           // this is really a concurrent modification. Hitting this exception 
means no pending
           // requests, if any, can succeed later, so it's safe to check status 
in strict mode
-          commitStatus = checkCommitStatusStrict(newMetadataLocation, 
tableMetadata);
+          commitStatus = checkCommitStatusStrict(newMetadataLocation, 
metadata);
           if (commitStatus == BaseMetastoreOperations.CommitStatus.FAILURE) {
             throw new CommitFailedException(
                 e, "The table %s.%s has been modified concurrently", database, 
tableName);
@@ -406,7 +392,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
               database,
               tableName,
               e);
-          commitStatus = checkCommitStatus(newMetadataLocation, tableMetadata);
+          commitStatus = checkCommitStatus(newMetadataLocation, metadata);
         }
 
         switch (commitStatus) {
diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
index 7cbe4e0c51a..49e86046960 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
@@ -275,6 +275,11 @@ public void doCommit(ViewMetadata base, ViewMetadata 
metadata) {
    */
   private boolean checkCurrentMetadataLocation(String newMetadataLocation) {
     ViewMetadata metadata = refresh();
+    if (metadata == null) {
+      // View creation may not have registered metadata yet.
+      return false;
+    }
+
     return newMetadataLocation.equals(metadata.metadataFileLocation());
   }
 
@@ -283,7 +288,8 @@ private static boolean hiveLockEnabled(Configuration conf) {
   }
 
   private Table newHMSView(ViewMetadata metadata) {
-    final long currentTimeMillis = System.currentTimeMillis();
+    // epoch seconds; HMS Thrift stores these as i32
+    final int currentTimeSeconds = (int) (System.currentTimeMillis() / 1000L);
     String hmsTableOwner =
         PropertyUtil.propertyAsString(
             metadata.properties(), HiveCatalog.HMS_TABLE_OWNER, 
HiveHadoopUtil.currentUser());
@@ -293,8 +299,8 @@ private Table newHMSView(ViewMetadata metadata) {
         table(),
         database(),
         hmsTableOwner,
-        (int) currentTimeMillis / 1000,
-        (int) currentTimeMillis / 1000,
+        currentTimeSeconds,
+        currentTimeSeconds,
         Integer.MAX_VALUE,
         null,
         Collections.emptyList(),
diff --git a/iceberg/iceberg-handler/pom.xml b/iceberg/iceberg-handler/pom.xml
index c991f807094..b82ce9983a9 100644
--- a/iceberg/iceberg-handler/pom.xml
+++ b/iceberg/iceberg-handler/pom.xml
@@ -171,6 +171,10 @@
       <artifactId>junit-vintage-engine</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>apache5-client</artifactId>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergAcidUtil.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergAcidUtil.java
index b881e07b2eb..a28423cdd9f 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergAcidUtil.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergAcidUtil.java
@@ -138,7 +138,7 @@ public static PositionDelete<Record> 
getPositionDelete(Record rec, Record rowDat
       rowData.set(i - dataOffset, rec.get(i));
     }
 
-    positionDelete.set(filePath, ObjectUtils.defaultIfNull(filePosition, 0L), 
rowData);
+    positionDelete.set(filePath, ObjectUtils.defaultIfNull(filePosition, 0L));
     return positionDelete;
   }
 
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveFileWriterFactory.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveFileWriterFactory.java
index 8923d825159..c72989264aa 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveFileWriterFactory.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveFileWriterFactory.java
@@ -19,47 +19,49 @@
 
 package org.apache.iceberg.mr.hive.writer;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.util.Map;
 import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.MetricsConfig;
+import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
-import org.apache.iceberg.SortOrder;
+import org.apache.iceberg.StructLike;
 import org.apache.iceberg.Table;
-import org.apache.iceberg.avro.Avro;
-import org.apache.iceberg.data.BaseFileWriterFactory;
+import org.apache.iceberg.data.GenericFileWriterFactory;
 import org.apache.iceberg.data.Record;
-import org.apache.iceberg.data.avro.DataWriter;
-import org.apache.iceberg.data.orc.GenericOrcWriter;
 import org.apache.iceberg.data.parquet.GenericParquetWriter;
-import org.apache.iceberg.orc.ORC;
+import org.apache.iceberg.deletes.EqualityDeleteWriter;
+import org.apache.iceberg.deletes.PositionDeleteWriter;
+import org.apache.iceberg.encryption.EncryptedOutputFile;
+import org.apache.iceberg.io.DataWriter;
+import org.apache.iceberg.io.FileWriterFactory;
 import org.apache.iceberg.parquet.Parquet;
 import org.apache.iceberg.parquet.VariantUtil;
 
-class HiveFileWriterFactory extends BaseFileWriterFactory<Record> {
+class HiveFileWriterFactory implements FileWriterFactory<Record> {
 
+  private final GenericFileWriterFactory delegate;
+  private final Table table;
+  private final FileFormat dataFileFormat;
+  private final Schema dataSchema;
   private final Map<String, String> properties;
   private Record sampleRecord = null;
 
   HiveFileWriterFactory(
-      Table table,
-      FileFormat dataFileFormat,
-      Schema dataSchema,
-      SortOrder dataSortOrder,
-      FileFormat deleteFileFormat,
-      int[] equalityFieldIds,
-      Schema equalityDeleteRowSchema,
-      SortOrder equalityDeleteSortOrder,
-      Schema positionDeleteRowSchema) {
-    super(
-        table,
-        dataFileFormat,
-        dataSchema,
-        dataSortOrder,
-        deleteFileFormat,
-        equalityFieldIds,
-        equalityDeleteRowSchema,
-        equalityDeleteSortOrder,
-        positionDeleteRowSchema);
-    properties = table.properties();
+      Table table, FileFormat dataFileFormat, Schema dataSchema, FileFormat 
deleteFileFormat) {
+    this.table = table;
+    this.dataFileFormat = dataFileFormat;
+    this.dataSchema = dataSchema;
+    this.properties = table.properties();
+
+    GenericFileWriterFactory.Builder builder =
+        new GenericFileWriterFactory.Builder(table)
+            .dataFileFormat(dataFileFormat)
+            .dataSchema(dataSchema)
+            .deleteFileFormat(deleteFileFormat);
+
+    this.delegate = builder.build();
   }
 
   static Builder builderFor(Table table) {
@@ -67,52 +69,41 @@ static Builder builderFor(Table table) {
   }
 
   @Override
-  protected void configureDataWrite(Avro.DataWriteBuilder builder) {
-    builder.createWriterFunc(DataWriter::create);
-  }
-
-  @Override
-  protected void configureEqualityDelete(Avro.DeleteWriteBuilder builder) {
-
-  }
-
-  @Override
-  protected void configurePositionDelete(Avro.DeleteWriteBuilder builder) {
-    builder.createWriterFunc(DataWriter::create);
-  }
-
-  @Override
-  protected void configureDataWrite(Parquet.DataWriteBuilder builder) {
-    builder.createWriterFunc(GenericParquetWriter::create);
-    // Configure variant shredding if enabled and a sample record is available
-    if (VariantUtil.shouldUseVariantShredding(properties, dataSchema())) {
-      
builder.variantShreddingFunc(VariantUtil.variantShreddingFunc(sampleRecord, 
dataSchema()));
+  public DataWriter<Record> newDataWriter(
+      EncryptedOutputFile file, PartitionSpec spec, StructLike partition) {
+    // Iceberg's GenericAppenderFactory/RegistryBasedFileWriterFactory does 
not currently support
+    // injecting a custom variantShreddingFunc, so we must instantiate 
Parquet.writeData directly
+    // when variant shredding is enabled for the table.
+    if (VariantUtil.shouldUseVariantShredding(dataFileFormat, properties, 
dataSchema)) {
+      try {
+        return Parquet.writeData(file)
+            .schema(dataSchema)
+            .createWriterFunc(GenericParquetWriter::create)
+            .setAll(properties)
+            .metricsConfig(MetricsConfig.forTable(table))
+            .withSpec(spec)
+            .withPartition(partition)
+            .withKeyMetadata(file.keyMetadata())
+            
.variantShreddingFunc(VariantUtil.variantShreddingFunc(sampleRecord, 
dataSchema))
+            .overwrite()
+            .build();
+      } catch (IOException e) {
+        throw new UncheckedIOException(e);
+      }
     }
+    return delegate.newDataWriter(file, spec, partition);
   }
 
   @Override
-  protected void configureEqualityDelete(Parquet.DeleteWriteBuilder builder) {
-
-  }
-
-  @Override
-  protected void configurePositionDelete(Parquet.DeleteWriteBuilder builder) {
-    builder.createWriterFunc(GenericParquetWriter::create);
-  }
-
-  @Override
-  protected void configureDataWrite(ORC.DataWriteBuilder builder) {
-    builder.createWriterFunc(GenericOrcWriter::buildWriter);
-  }
-
-  @Override
-  protected void configureEqualityDelete(ORC.DeleteWriteBuilder 
deleteWriteBuilder) {
-
+  public EqualityDeleteWriter<Record> newEqualityDeleteWriter(
+      EncryptedOutputFile file, PartitionSpec spec, StructLike partition) {
+    return delegate.newEqualityDeleteWriter(file, spec, partition);
   }
 
   @Override
-  protected void configurePositionDelete(ORC.DeleteWriteBuilder 
deleteWriteBuilder) {
-    deleteWriteBuilder.createWriterFunc(GenericOrcWriter::buildWriter);
+  public PositionDeleteWriter<Record> newPositionDeleteWriter(
+      EncryptedOutputFile file, PartitionSpec spec, StructLike partition) {
+    return delegate.newPositionDeleteWriter(file, spec, partition);
   }
 
   static class Builder {
@@ -120,7 +111,6 @@ static class Builder {
     private FileFormat dataFileFormat;
     private Schema dataSchema;
     private FileFormat deleteFileFormat;
-    private Schema positionDeleteRowSchema;
 
     Builder(Table table) {
       this.table = table;
@@ -141,22 +131,8 @@ Builder deleteFileFormat(FileFormat newDeleteFileFormat) {
       return this;
     }
 
-    Builder positionDeleteRowSchema(Schema newPositionDeleteRowSchema) {
-      this.positionDeleteRowSchema = newPositionDeleteRowSchema;
-      return this;
-    }
-
     HiveFileWriterFactory build() {
-      return new HiveFileWriterFactory(
-          table,
-          dataFileFormat,
-          dataSchema,
-          null,
-          deleteFileFormat,
-          null,
-          null,
-          null,
-          positionDeleteRowSchema);
+      return new HiveFileWriterFactory(table, dataFileFormat, dataSchema, 
deleteFileFormat);
     }
   }
 
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergCopyOnWriteRecordWriter.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergCopyOnWriteRecordWriter.java
index cdb03e1a89c..cd058bbd1f5 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergCopyOnWriteRecordWriter.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergCopyOnWriteRecordWriter.java
@@ -55,7 +55,7 @@ class HiveIcebergCopyOnWriteRecordWriter extends 
HiveIcebergDefaultWriter {
   public void write(Writable row) throws IOException {
     Record record = ((Container<Record>) row).get();
     PositionDelete<Record> positionDelete = 
IcebergAcidUtil.getPositionDelete(record, rowDataTemplate, false);
-    Record rowData = positionDelete.row();
+    Record rowData = rowDataTemplate;
 
     if (positionDelete.pos() < 0) {
       int specId = IcebergAcidUtil.parseSpecId(record);
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergDeleteWriter.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergDeleteWriter.java
index 6970556fc46..f4526189e86 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergDeleteWriter.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/HiveIcebergDeleteWriter.java
@@ -61,10 +61,10 @@ public void write(Writable row) throws IOException {
     PositionDelete<Record> positionDelete = 
IcebergAcidUtil.getPositionDelete(rec, rowDataTemplate, isMergeTask);
     int specId = IcebergAcidUtil.parseSpecId(rec);
     PartitionKey partitionKey = isMergeTask ? 
IcebergAcidUtil.parsePartitionKey(rec) :
-        partition(positionDelete.row(), specId);
+        partition(rowDataTemplate, specId);
     if (skipRowData) {
       // Set null as the row data as we intend to avoid writing the actual row 
data in the delete file.
-      positionDelete.set(positionDelete.path(), positionDelete.pos(), null);
+      positionDelete.set(positionDelete.path(), positionDelete.pos());
     }
     writer.write(positionDelete, specs.get(specId), partitionKey);
   }
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/WriterBuilder.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/WriterBuilder.java
index dbe10bc4401..20386be3ffe 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/WriterBuilder.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/writer/WriterBuilder.java
@@ -132,9 +132,6 @@ public HiveIcebergWriter build() {
         .dataFileFormat(context.dataFileFormat())
         .dataSchema(shouldAddRowLineageColumns ? 
MetadataColumns.schemaWithRowLineage(table.schema()) : table.schema())
         .deleteFileFormat(context.deleteFileFormat())
-        .positionDeleteRowSchema(context.skipRowData() || 
!context.inputOrdered() ?
-            // SortingPositionOnlyDeleteWriter doesn't support rawData in 
delete schema
-            null : table.schema())
         .build();
 
     HiveIcebergWriter writer;
@@ -234,11 +231,11 @@ static class Context {
     Context(Map<String, String> properties, UnaryOperator<String> ops, String 
tableName) {
       String dataFileFormatName =
           properties.getOrDefault(DEFAULT_FILE_FORMAT, 
DEFAULT_FILE_FORMAT_DEFAULT);
-      this.dataFileFormat = 
FileFormat.valueOf(dataFileFormatName.toUpperCase(Locale.ENGLISH));
+      this.dataFileFormat = 
FileFormat.valueOf(dataFileFormatName.toUpperCase(Locale.ROOT));
 
       String deleteFileFormatName =
           properties.getOrDefault(DELETE_DEFAULT_FILE_FORMAT, 
dataFileFormatName);
-      this.deleteFileFormat = 
FileFormat.valueOf(deleteFileFormatName.toUpperCase(Locale.ENGLISH));
+      this.deleteFileFormat = 
FileFormat.valueOf(deleteFileFormatName.toUpperCase(Locale.ROOT));
 
       this.targetDataFileSize = PropertyUtil.propertyAsLong(properties,
           TableProperties.WRITE_TARGET_FILE_SIZE_BYTES, 
TableProperties.WRITE_TARGET_FILE_SIZE_BYTES_DEFAULT);
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/parquet/VariantUtil.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/parquet/VariantUtil.java
index 736a39b895c..fa9a41feb8a 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/parquet/VariantUtil.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/parquet/VariantUtil.java
@@ -22,6 +22,7 @@
 import java.util.List;
 import java.util.Map;
 import org.apache.iceberg.Accessor;
+import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.data.Record;
@@ -89,8 +90,11 @@ private static List<VariantField> 
variantFieldsForShredding(Schema schema) {
     return results;
   }
 
-  public static boolean shouldUseVariantShredding(Map<String, String> 
properties, Schema schema) {
-    return isVariantShreddingEnabled(properties) && hasVariantFields(schema);
+  public static boolean shouldUseVariantShredding(
+      FileFormat fileFormat, Map<String, String> properties, Schema schema) {
+    return fileFormat == FileFormat.PARQUET &&
+        isVariantShreddingEnabled(properties) &&
+        hasVariantFields(schema);
   }
 
   private static boolean hasVariantFields(Schema schema) {
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/FileHelpers.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/FileHelpers.java
index 5f071f5b885..563363f2f26 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/FileHelpers.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/FileHelpers.java
@@ -22,14 +22,16 @@
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import org.apache.iceberg.DataFile;
-import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.DeleteFile;
 import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.FileMetadata;
+import org.apache.iceberg.MetricsConfig;
+import org.apache.iceberg.PartitionData;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.Table;
+import org.apache.iceberg.data.parquet.GenericParquetWriter;
 import org.apache.iceberg.deletes.BaseDVFileWriter;
 import org.apache.iceberg.deletes.DVFileWriter;
 import org.apache.iceberg.deletes.EqualityDeleteWriter;
@@ -38,98 +40,98 @@
 import org.apache.iceberg.encryption.EncryptedFiles;
 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.FileAppender;
-import org.apache.iceberg.io.FileAppenderFactory;
 import org.apache.iceberg.io.FileWriterFactory;
 import org.apache.iceberg.io.OutputFile;
 import org.apache.iceberg.io.OutputFileFactory;
+import org.apache.iceberg.parquet.Parquet;
 import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.util.CharSequenceSet;
 import org.apache.iceberg.util.Pair;
 
-import static org.apache.iceberg.TableProperties.DEFAULT_FILE_FORMAT;
-import static org.apache.iceberg.TableProperties.DEFAULT_FILE_FORMAT_DEFAULT;
-
 public class FileHelpers {
   private FileHelpers() {
   }
 
-  public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(Table table, 
OutputFile out,
-                                                                    
List<Pair<CharSequence, Long>> deletes)
-      throws IOException {
-    return writeDeleteFile(table, out, null, deletes);
-  }
-
-  public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(Table table, 
OutputFile out, StructLike partition,
-                                                                    
List<Pair<CharSequence, Long>> deletes)
+  public static DeleteFile testOnlyPosDeleteFileWithRow(
+      Table table, OutputFile out, StructLike partition, List<Record> 
posDeleteRecords)
       throws IOException {
-    FileFormat format = defaultFormat(table.properties());
-    FileAppenderFactory<Record> factory = new 
GenericAppenderFactory(table.schema(), table.spec());
+    Schema posDeleteSchema = DeleteSchemaUtil.posDeleteSchema(table.schema());
+    FileAppender<Record> appender =
+        Parquet.write(out)
+            .schema(posDeleteSchema)
+            .createWriterFunc(GenericParquetWriter::create)
+            .metricsConfig(MetricsConfig.forPositionDelete())
+            .overwrite()
+            .build();
+    try (Closeable ignored = appender) {
+      posDeleteRecords.forEach(appender::add);
+    }
 
-    PositionDeleteWriter<Record> writer =
-        factory.newPosDeleteWriter(encrypt(out), format, partition);
-    PositionDelete<Record> posDelete = PositionDelete.create();
-    try (Closeable toClose = writer) {
-      for (Pair<CharSequence, Long> delete : deletes) {
-        writer.write(posDelete.set(delete.first(), delete.second(), null));
-      }
+    FileMetadata.Builder builder =
+        FileMetadata.deleteFileBuilder(table.spec())
+            .ofPositionDeletes()
+            .withFormat(FileFormat.PARQUET)
+            .withPath(out.location())
+            .withFileSizeInBytes(appender.length())
+            .withMetrics(appender.metrics());
+    if (partition != null) {
+      builder.withPartition(partition);
     }
 
-    return Pair.of(writer.toDeleteFile(), writer.referencedDataFiles());
+    return builder.build();
   }
 
-  public static DeleteFile writeDeleteFile(Table table, OutputFile out, 
List<Record> deletes, Schema deleteRowSchema)
-      throws IOException {
-    return writeDeleteFile(table, out, null, deletes, deleteRowSchema);
+  public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(
+      Table table, OutputFile out, List<Pair<CharSequence, Long>> deletes) 
throws IOException {
+    return writeDeleteFile(table, out, null, deletes);
   }
 
-  public static DeleteFile writeDeleteFile(Table table, OutputFile out, 
StructLike partition,
-                                           List<Record> deletes, Schema 
deleteRowSchema)
+  public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(
+      Table table, OutputFile out, List<Pair<CharSequence, Long>> deletes, int 
formatVersion)
       throws IOException {
-    FileFormat format = defaultFormat(table.properties());
-    int[] equalityFieldIds = 
deleteRowSchema.columns().stream().mapToInt(Types.NestedField::fieldId).toArray();
-    FileAppenderFactory<Record> factory = new 
GenericAppenderFactory(table.schema(), table.spec(),
-        equalityFieldIds, deleteRowSchema, null);
-
-    EqualityDeleteWriter<Record> writer = 
factory.newEqDeleteWriter(encrypt(out), format, partition);
-    try (Closeable toClose = writer) {
-      writer.write(deletes);
-    }
+    return writeDeleteFile(table, out, null, deletes, formatVersion);
+  }
 
-    return writer.toDeleteFile();
+  public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(
+      Table table, OutputFile out, StructLike partition, 
List<Pair<CharSequence, Long>> deletes)
+      throws IOException {
+    return writeDeleteFile(table, out, partition, deletes, 2);
   }
 
   public static Pair<DeleteFile, CharSequenceSet> writeDeleteFile(
-          Table table,
-          OutputFile out,
-          StructLike partition,
-          List<Pair<CharSequence, Long>> deletes,
-          int formatVersion)
-          throws IOException {
+      Table table,
+      OutputFile out,
+      StructLike partition,
+      List<Pair<CharSequence, Long>> deletes,
+      int formatVersion)
+      throws IOException {
     if (formatVersion >= 3) {
       OutputFileFactory fileFactory =
-              OutputFileFactory.builderFor(table, 1, 
1).format(FileFormat.PUFFIN).build();
+          OutputFileFactory.builderFor(table, 1, 
1).format(FileFormat.PUFFIN).build();
       DVFileWriter writer = new BaseDVFileWriter(fileFactory, p -> null);
       try (DVFileWriter closeableWriter = writer) {
         for (Pair<CharSequence, Long> delete : deletes) {
           closeableWriter.delete(
-                  delete.first().toString(), delete.second(), table.spec(), 
partition);
+              delete.first().toString(), delete.second(), table.spec(), 
partition);
         }
       }
 
       return Pair.of(
-              Iterables.getOnlyElement(writer.result().deleteFiles()),
-              writer.result().referencedDataFiles());
+          Iterables.getOnlyElement(writer.result().deleteFiles()),
+          writer.result().referencedDataFiles());
     } else {
       FileWriterFactory<Record> factory = 
GenericFileWriterFactory.builderFor(table).build();
 
       PositionDeleteWriter<Record> writer =
-              factory.newPositionDeleteWriter(encrypt(out), table.spec(), 
partition);
+          factory.newPositionDeleteWriter(encrypt(out), table.spec(), 
partition);
       PositionDelete<Record> posDelete = PositionDelete.create();
       try (Closeable toClose = writer) {
         for (Pair<CharSequence, Long> delete : deletes) {
-          writer.write(posDelete.set(delete.first(), delete.second(), null));
+          writer.write(posDelete.set(delete.first(), delete.second()));
         }
       }
 
@@ -137,60 +139,81 @@ public static Pair<DeleteFile, CharSequenceSet> 
writeDeleteFile(
     }
   }
 
-  public static DataFile writeDataFile(Table table, OutputFile out, 
List<Record> rows) throws IOException {
-    FileFormat format = defaultFormat(table.properties());
-    GenericAppenderFactory factory = new 
GenericAppenderFactory(table.schema());
+  public static DeleteFile writeDeleteFile(
+      Table table, OutputFile out, List<Record> deletes, Schema 
deleteRowSchema)
+      throws IOException {
+    return writeDeleteFile(table, out, null, deletes, deleteRowSchema);
+  }
 
-    FileAppender<Record> writer = factory.newAppender(out, format);
+  public static DeleteFile writeDeleteFile(
+      Table table,
+      OutputFile out,
+      StructLike partition,
+      List<Record> deletes,
+      Schema deleteRowSchema)
+      throws IOException {
+    int[] equalityFieldIds =
+        
deleteRowSchema.columns().stream().mapToInt(Types.NestedField::fieldId).toArray();
+    FileWriterFactory<Record> factory =
+        GenericFileWriterFactory.builderFor(table)
+            .equalityDeleteRowSchema(deleteRowSchema)
+            .equalityFieldIds(equalityFieldIds)
+            .build();
+
+    EqualityDeleteWriter<Record> writer =
+        factory.newEqualityDeleteWriter(encrypt(out), table.spec(), partition);
     try (Closeable toClose = writer) {
-      writer.addAll(rows);
+      writer.write(deletes);
     }
 
-    return DataFiles.builder(table.spec())
-        .withFormat(format)
-        .withPath(out.location())
-        .withFileSizeInBytes(writer.length())
-        .withSplitOffsets(writer.splitOffsets())
-        .withMetrics(writer.metrics())
-        .build();
+    return writer.toDeleteFile();
   }
 
-  public static DataFile writeDataFile(Table table, OutputFile out, StructLike 
partition, List<Record> rows)
+  public static DataFile writeDataFile(Table table, OutputFile out, 
List<Record> rows)
       throws IOException {
-    FileFormat format = defaultFormat(table.properties());
-    GenericAppenderFactory factory = new 
GenericAppenderFactory(table.schema(), table.spec());
+    return writeDataFile(table, out, rows, null);
+  }
+
+  public static DataFile writeDataFile(
+      Table table, OutputFile out, List<Record> rows, PartitionData partition) 
throws IOException {
+    FileWriterFactory<Record> factory = 
GenericFileWriterFactory.builderFor(table).build();
+
+    DataWriter<Record> writer = factory.newDataWriter(encrypt(out), 
table.spec(), partition);
+    try (Closeable toClose = writer) {
+      writer.write(rows);
+    }
+
+    return writer.toDataFile();
+  }
+
+  public static DataFile writeDataFile(
+      Table table, OutputFile out, StructLike partition, List<Record> rows) 
throws IOException {
+    FileWriterFactory<Record> factory = 
GenericFileWriterFactory.builderFor(table).build();
 
-    FileAppender<Record> writer = factory.newAppender(out, format);
+    DataWriter<Record> writer = factory.newDataWriter(encrypt(out), 
table.spec(), partition);
     try (Closeable toClose = writer) {
-      writer.addAll(rows);
+      writer.write(rows);
     }
 
-    return DataFiles.builder(table.spec())
-        .withFormat(format)
-        .withPath(out.location())
-        .withPartition(partition)
-        .withFileSizeInBytes(writer.length())
-        .withSplitOffsets(writer.splitOffsets())
-        .withMetrics(writer.metrics())
-        .build();
+    return writer.toDataFile();
   }
 
   public static DeleteFile writePosDeleteFile(
-          Table table, OutputFile out, StructLike partition, 
List<PositionDelete<?>> deletes)
-          throws IOException {
+      Table table, OutputFile out, StructLike partition, 
List<PositionDelete<?>> deletes)
+      throws IOException {
     return writePosDeleteFile(table, out, partition, deletes, 2);
   }
 
   public static DeleteFile writePosDeleteFile(
-          Table table,
-          OutputFile out,
-          StructLike partition,
-          List<PositionDelete<?>> deletes,
-          int formatVersion)
-          throws IOException {
+      Table table,
+      OutputFile out,
+      StructLike partition,
+      List<PositionDelete<?>> deletes,
+      int formatVersion)
+      throws IOException {
     if (formatVersion >= 3) {
       OutputFileFactory fileFactory =
-              OutputFileFactory.builderFor(table, 1, 
1).format(FileFormat.PUFFIN).build();
+          OutputFileFactory.builderFor(table, 1, 
1).format(FileFormat.PUFFIN).build();
       DVFileWriter writer = new BaseDVFileWriter(fileFactory, p -> null);
       try (DVFileWriter closeableWriter = writer) {
         for (PositionDelete<?> delete : deletes) {
@@ -200,13 +223,10 @@ public static DeleteFile writePosDeleteFile(
 
       return Iterables.getOnlyElement(writer.result().deleteFiles());
     } else {
-      FileWriterFactory<Record> factory =
-              GenericFileWriterFactory.builderFor(table)
-                      .positionDeleteRowSchema(table.schema())
-                      .build();
+      FileWriterFactory<Record> factory = 
GenericFileWriterFactory.builderFor(table).build();
 
       PositionDeleteWriter<?> writer =
-              factory.newPositionDeleteWriter(encrypt(out), table.spec(), 
partition);
+          factory.newPositionDeleteWriter(encrypt(out), table.spec(), 
partition);
       try (Closeable toClose = writer) {
         for (PositionDelete delete : deletes) {
           writer.write(delete);
@@ -217,12 +237,7 @@ public static DeleteFile writePosDeleteFile(
     }
   }
 
-  private static EncryptedOutputFile encrypt(OutputFile out) {
+  public static EncryptedOutputFile encrypt(OutputFile out) {
     return EncryptedFiles.encryptedOutput(out, EncryptionKeyMetadata.EMPTY);
   }
-
-  private static FileFormat defaultFormat(Map<String, String> properties) {
-    String formatString = properties.getOrDefault(DEFAULT_FILE_FORMAT, 
DEFAULT_FILE_FORMAT_DEFAULT);
-    return FileFormat.fromString(formatString);
-  }
 }
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/GenericAppenderHelper.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/GenericAppenderHelper.java
index c1313041802..902943e9a8d 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/GenericAppenderHelper.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/data/GenericAppenderHelper.java
@@ -21,43 +21,58 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.List;
+import java.util.UUID;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.iceberg.AppendFiles;
 import org.apache.iceberg.DataFile;
-import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.Files;
+import org.apache.iceberg.SnapshotRef;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.Table;
-import org.apache.iceberg.io.FileAppender;
+import org.apache.iceberg.io.DataWriter;
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
-import org.junit.Assert;
 import org.junit.rules.TemporaryFolder;
 
-/**
- * Helper for appending {@link DataFile} to a table or appending {@link 
Record}s to a table.
- */
+import static org.apache.iceberg.data.FileHelpers.encrypt;
+
+/** Helper for appending {@link DataFile} to a table or appending {@link 
Record}s to a table. */
 public class GenericAppenderHelper {
 
   private static final String ORC_CONFIG_PREFIX = "^orc.*";
+  private static final String PARQUET_CONFIG_PATTERN = ".*parquet.*";
 
   private final Table table;
   private final FileFormat fileFormat;
-  private final TemporaryFolder tmp;
+  private final Path temp;
   private final Configuration conf;
 
   public GenericAppenderHelper(Table table, FileFormat fileFormat, 
TemporaryFolder tmp, Configuration conf) {
+    this(table, fileFormat, tmp != null ? tmp.getRoot().toPath() : null, conf);
+  }
+
+  public GenericAppenderHelper(Table table, FileFormat fileFormat, 
TemporaryFolder tmp) {
+    this(table, fileFormat, tmp, null);
+  }
+
+  public GenericAppenderHelper(Table table, FileFormat fileFormat, Path temp, 
Configuration conf) {
     this.table = table;
     this.fileFormat = fileFormat;
-    this.tmp = tmp;
+    this.temp = temp;
     this.conf = conf;
   }
 
-  public void appendToTable(DataFile... dataFiles) {
+  public GenericAppenderHelper(Table table, FileFormat fileFormat, Path temp) {
+    this(table, fileFormat, temp, null);
+  }
+
+  public void appendToTable(String branch, DataFile... dataFiles) {
     Preconditions.checkNotNull(table, "table not set");
 
-    AppendFiles append = table.newAppend();
+    AppendFiles append =
+        table.newAppend().toBranch(branch != null ? branch : 
SnapshotRef.MAIN_BRANCH);
 
     for (DataFile dataFile : dataFiles) {
       append = append.appendFile(dataFile);
@@ -66,45 +81,68 @@ public void appendToTable(DataFile... dataFiles) {
     append.commit();
   }
 
+  public void appendToTable(DataFile... dataFiles) {
+    appendToTable(null, dataFiles);
+  }
+
   public void appendToTable(List<Record> records) throws IOException {
-    appendToTable(null, records);
+    appendToTable(null, null, records);
+  }
+
+  public void appendToTable(String branch, List<Record> records) throws 
IOException {
+    appendToTable(null, branch, records);
+  }
+
+  public void appendToTable(StructLike partition, String branch, List<Record> 
records)
+      throws IOException {
+    appendToTable(branch, writeFile(partition, records));
   }
 
   public void appendToTable(StructLike partition, List<Record> records) throws 
IOException {
     appendToTable(writeFile(partition, records));
   }
 
+  public DataFile writeFile(List<Record> records) throws IOException {
+    Preconditions.checkNotNull(table, "table not set");
+    File file = temp.resolve("generic-appender-test-" + 
UUID.randomUUID()).toFile();
+    return appendToLocalFile(table, file, fileFormat, null, records, conf);
+  }
+
   public DataFile writeFile(StructLike partition, List<Record> records) throws 
IOException {
     Preconditions.checkNotNull(table, "table not set");
-    File file = tmp.newFile();
-    Assert.assertTrue(file.delete());
+    File file =
+        temp.resolve("generic-appender-partition-test-" + 
UUID.randomUUID()).toFile();
     return appendToLocalFile(table, file, fileFormat, partition, records, 
conf);
   }
 
-  private static DataFile appendToLocalFile(Table table, File file, FileFormat 
format, StructLike partition,
-      List<Record> records, Configuration conf)
+  private static DataFile appendToLocalFile(
+      Table table,
+      File file,
+      FileFormat format,
+      StructLike partition,
+      List<Record> records,
+      Configuration conf)
       throws IOException {
-    GenericAppenderFactory appenderFactory = new 
GenericAppenderFactory(table.schema());
+    GenericFileWriterFactory.Builder builder =
+        new GenericFileWriterFactory.Builder(table).dataFileFormat(format);
+
+    // Push down ORC related settings to appender if there are any
+    if (FileFormat.ORC.equals(format) && conf != null) {
+      builder.writerProperties(conf.getValByRegex(ORC_CONFIG_PREFIX));
+    }
 
-    // Push down ORC related settings to appender
-    if (FileFormat.ORC.equals(format)) {
-      appenderFactory.setAll(conf.getValByRegex(ORC_CONFIG_PREFIX));
+    if (FileFormat.PARQUET.equals(format) && conf != null) {
+      builder.writerProperties(conf.getValByRegex(PARQUET_CONFIG_PATTERN));
     }
 
-    FileAppender<Record> appender = appenderFactory.newAppender(
-        Files.localOutput(file), format);
-    try (FileAppender<Record> fileAppender = appender) {
-      fileAppender.addAll(records);
+    DataWriter<Record> writer =
+        builder.build().newDataWriter(encrypt(Files.localOutput(file)), 
table.spec(), partition);
+    try (writer) {
+      for (Record r : records) {
+        writer.write(r);
+      }
     }
 
-    return DataFiles.builder(table.spec())
-        .withRecordCount(records.size())
-        .withFileSizeInBytes(file.length())
-        .withPath(Files.localInput(file).location())
-        .withMetrics(appender.metrics())
-        .withFormat(format)
-        .withPartition(partition)
-        .withSplitOffsets(appender.splitOffsets())
-        .build();
+    return writer.toDataFile();
   }
 }
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergCRUD.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergCRUD.java
index 1826d241a01..8b3fbfbc464 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergCRUD.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergCRUD.java
@@ -21,17 +21,23 @@
 
 import java.io.IOException;
 import java.util.List;
+import java.util.UUID;
 import java.util.stream.StreamSupport;
 import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DeleteFile;
 import org.apache.iceberg.FileFormat;
+import org.apache.iceberg.MetadataColumns;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.data.FileHelpers;
+import org.apache.iceberg.data.GenericRecord;
 import org.apache.iceberg.data.Record;
 import org.apache.iceberg.deletes.PositionDelete;
+import org.apache.iceberg.io.DeleteSchemaUtil;
+import org.apache.iceberg.io.OutputFile;
 import org.apache.iceberg.mr.TestHelper;
 import org.apache.iceberg.mr.hive.test.TestTables.TestTableType;
 import 
org.apache.iceberg.mr.hive.test.utils.HiveIcebergStorageHandlerTestUtils;
@@ -142,9 +148,7 @@ public void 
testReadAndWriteFormatV2Unpartitioned_PosDelete() throws IOException
     DataFile dataFile = 
StreamSupport.stream(tbl.currentSnapshot().addedDataFiles(tbl.io()).spliterator(),
 false)
         .findFirst()
         .orElseThrow(() -> new RuntimeException("Did not find any data files 
for test table"));
-    List<PositionDelete<Record>> deletes = ImmutableList.of(positionDelete(
-        dataFile.path(), 2L, 
HiveIcebergStorageHandlerTestUtils.CUSTOMER_RECORDS.get(2))
-    );
+    List<PositionDelete<Record>> deletes = 
ImmutableList.of(positionDelete(dataFile.path(), 2L));
     DeleteFile deleteFile = 
HiveIcebergTestUtils.createPositionalDeleteFile(tbl, "dummyPath",
         fileFormat, null, deletes);
     tbl.newRowDelta().addDeletes(deleteFile).commit();
@@ -158,7 +162,7 @@ public void 
testReadAndWriteFormatV2Unpartitioned_PosDelete() throws IOException
   }
 
   @Test
-  public void testReadAndWriteFormatV2Partitioned_PosDelete_RowNotSupplied() 
throws IOException {
+  public void testReadAndWriteFormatV2Partitioned_PosDelete() throws 
IOException {
     Assume.assumeTrue(formatVersion == 2);
 
     PartitionSpec spec = 
PartitionSpec.builderFor(HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA)
@@ -171,15 +175,15 @@ public void 
testReadAndWriteFormatV2Partitioned_PosDelete_RowNotSupplied() throw
         "(0, 'Blake', 'Blue')");
     tbl.refresh();
 
-    // delete the first and third rows from the newly-added data file - with 
row supplied
+    // delete the first and third rows from the newly-added data file
     DataFile dataFile = 
StreamSupport.stream(tbl.currentSnapshot().addedDataFiles(tbl.io()).spliterator(),
 false)
         .filter(file -> file.partition().get(0, Long.class) == 0L)
         .filter(file -> file.recordCount() == 3)
         .findAny()
         .orElseThrow(() -> new RuntimeException("Did not find the desired data 
file in the test table"));
     List<PositionDelete<Record>> deletes = ImmutableList.of(
-        positionDelete(dataFile.path(), 0L, null),
-        positionDelete(dataFile.path(), 2L, null)
+        positionDelete(dataFile.path(), 0L),
+        positionDelete(dataFile.path(), 2L)
     );
     DeleteFile deleteFile = 
HiveIcebergTestUtils.createPositionalDeleteFile(tbl, "dummyPath",
         fileFormat, ImmutableMap.of("customer_id", 0L), deletes);
@@ -194,9 +198,15 @@ public void 
testReadAndWriteFormatV2Partitioned_PosDelete_RowNotSupplied() throw
     Assert.assertArrayEquals(new Object[] {2L, "Trudy", "Pink"}, 
objects.get(3));
   }
 
+  /**
+   * Tests reading/consuming legacy Iceberg V2 tables containing position 
delete files with embedded
+   * row data. Iceberg 1.12.0 removed writer support for row data in position 
deletes, but Hive
+   * readers must remain backward compatible and safely ignore the embedded 
row field when
+   * projecting deleted positions.
+   */
   @Test
-  public void testReadAndWriteFormatV2Partitioned_PosDelete_RowSupplied() 
throws IOException {
-    Assume.assumeTrue(formatVersion == 2);
+  public void testConsumeLegacyFormatV2Partitioned_PosDelete_RowSupplied() 
throws IOException {
+    Assume.assumeTrue(formatVersion == 2 && fileFormat == FileFormat.PARQUET);
 
     PartitionSpec spec = 
PartitionSpec.builderFor(HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA)
         .identity("customer_id").build();
@@ -214,18 +224,35 @@ public void 
testReadAndWriteFormatV2Partitioned_PosDelete_RowSupplied() throws I
         .filter(file -> file.recordCount() == 3)
         .findAny()
         .orElseThrow(() -> new RuntimeException("Did not find the desired data 
file in the test table"));
+
+    // Construct a legacy position delete file with embedded row data (fields: 
file_path, pos, row).
+    // FileHelpers.testOnlyPosDeleteFileWithRow writes this directly without 
using the standard delete writer,
+    // which disallows writing row data in Iceberg 1.12.0+.
+    Schema posDeleteSchema = DeleteSchemaUtil.posDeleteSchema(tbl.schema());
     List<Record> rowsToDel = 
TestHelper.RecordsBuilder.newInstance(HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA)
-        .add(0L, "Laura", "Yellow").add(0L, "Blake", "Blue").build();
-    List<PositionDelete<Record>> deletes = ImmutableList.of(
-        positionDelete(dataFile.path(), 0L, rowsToDel.get(0)),
-        positionDelete(dataFile.path(), 2L, rowsToDel.get(1))
-    );
-    DeleteFile deleteFile = 
HiveIcebergTestUtils.createPositionalDeleteFile(tbl, "dummyPath",
-        fileFormat, ImmutableMap.of("customer_id", 0L), deletes);
+        .add(0L, "Laura", "Yellow")
+        .add(0L, "Blake", "Blue")
+        .build();
+
+    Record delRecord0 = GenericRecord.create(posDeleteSchema);
+    delRecord0.setField(MetadataColumns.DELETE_FILE_PATH.name(), 
dataFile.path().toString());
+    delRecord0.setField(MetadataColumns.DELETE_FILE_POS.name(), 0L);
+    delRecord0.setField(MetadataColumns.DELETE_FILE_ROW_FIELD_NAME, 
rowsToDel.get(0));
+
+    Record delRecord2 = GenericRecord.create(posDeleteSchema);
+    delRecord2.setField(MetadataColumns.DELETE_FILE_PATH.name(), 
dataFile.path().toString());
+    delRecord2.setField(MetadataColumns.DELETE_FILE_POS.name(), 2L);
+    delRecord2.setField(MetadataColumns.DELETE_FILE_ROW_FIELD_NAME, 
rowsToDel.get(1));
+
+    OutputFile deleteOutputFile = tbl.io().newOutputFile(
+        tbl.location() + "/data/dummyLegacyPosDelete_" + UUID.randomUUID() + 
".parquet");
+    DeleteFile deleteFile = FileHelpers.testOnlyPosDeleteFileWithRow(
+        tbl, deleteOutputFile, dataFile.partition(), 
ImmutableList.of(delRecord0, delRecord2));
     tbl.newRowDelta().addDeletes(deleteFile).commit();
 
     List<Object[]> objects = shell.executeStatement("SELECT * FROM customers 
ORDER BY customer_id, first_name");
 
+    // Verify that Hive readers successfully filter out deleted rows (0: 
Alice, 0: John, 1: Bob, 2: Trudy)
     Assert.assertEquals(4, objects.size());
     Assert.assertArrayEquals(new Object[] {0L, "Alice", "Brown"}, 
objects.get(0));
     Assert.assertArrayEquals(new Object[] {0L, "John", "Green"}, 
objects.get(1));
@@ -620,8 +647,9 @@ public void testMultiInsert() {
     Assert.assertEquals(1, res.size());
   }
 
-  private static <T> PositionDelete<T> positionDelete(CharSequence path, long 
pos, T row) {
+  private static <T> PositionDelete<T> positionDelete(CharSequence path, long 
pos) {
     PositionDelete<T> positionDelete = PositionDelete.create();
-    return positionDelete.set(path, pos, row);
+    positionDelete.set(path, pos);
+    return positionDelete;
   }
 }
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/test/utils/HiveIcebergTestUtils.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/test/utils/HiveIcebergTestUtils.java
index 2e3d2106874..87033a3a709 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/test/utils/HiveIcebergTestUtils.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/test/utils/HiveIcebergTestUtils.java
@@ -68,7 +68,7 @@
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Table;
-import org.apache.iceberg.data.GenericAppenderFactory;
+import org.apache.iceberg.data.GenericFileWriterFactory;
 import org.apache.iceberg.data.GenericRecord;
 import org.apache.iceberg.data.IcebergGenerics;
 import org.apache.iceberg.data.Record;
@@ -78,7 +78,7 @@
 import org.apache.iceberg.encryption.EncryptedOutputFile;
 import org.apache.iceberg.hadoop.HadoopOutputFile;
 import org.apache.iceberg.io.CloseableIterable;
-import org.apache.iceberg.io.FileAppenderFactory;
+import org.apache.iceberg.io.FileWriterFactory;
 import org.apache.iceberg.mr.hive.HiveTableUtil;
 import org.apache.iceberg.mr.hive.test.TestHiveShell;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
@@ -364,8 +364,12 @@ public static DeleteFile createEqualityDeleteFile(Table 
table, PartitionSpec spe
         .toList();
     Schema eqDeleteRowSchema = 
table.schema().select(equalityFields.toArray(new String[]{}));
 
-    FileAppenderFactory<Record> appenderFactory = new 
GenericAppenderFactory(table.schema(), spec,
-        ArrayUtil.toIntArray(equalityFieldIds), eqDeleteRowSchema, null);
+    FileWriterFactory<Record> writerFactory =
+        new GenericFileWriterFactory.Builder(table)
+            .deleteFileFormat(fileFormat)
+            .equalityFieldIds(ArrayUtil.toIntArray(equalityFieldIds))
+            .equalityDeleteRowSchema(eqDeleteRowSchema)
+            .build();
     EncryptedOutputFile outputFile = 
table.encryption().encrypt(HadoopOutputFile.fromPath(
         new org.apache.hadoop.fs.Path(table.location(), deleteFilePath), new 
Configuration()));
 
@@ -374,9 +378,10 @@ public static DeleteFile createEqualityDeleteFile(Table 
table, PartitionSpec spe
       part = new PartitionKey(spec, eqDeleteRowSchema);
       part.partition(rowsToDelete.get(0));
     }
-    EqualityDeleteWriter<Record> eqWriter = 
appenderFactory.newEqDeleteWriter(outputFile, fileFormat, part);
-    try (EqualityDeleteWriter<Record> writer = eqWriter) {
-      writer.write(rowsToDelete);
+    EqualityDeleteWriter<Record> eqWriter =
+        writerFactory.newEqualityDeleteWriter(outputFile, spec, part);
+    try (eqWriter) {
+      eqWriter.write(rowsToDelete);
     }
     return eqWriter.toDeleteFile();
   }
@@ -394,9 +399,8 @@ public static DeleteFile createEqualityDeleteFile(Table 
table, PartitionSpec spe
   public static DeleteFile createPositionalDeleteFile(Table table, String 
deleteFilePath, FileFormat fileFormat,
       Map<String, Object> partitionValues, List<PositionDelete<Record>> 
deletes) throws IOException {
 
-    Schema posDeleteRowSchema = deletes.get(0).row() == null ? null : 
table.schema();
-    FileAppenderFactory<Record> appenderFactory = new 
GenericAppenderFactory(table.schema(), table.spec(),
-        null, null, posDeleteRowSchema);
+    FileWriterFactory<Record> writerFactory =
+        new 
GenericFileWriterFactory.Builder(table).deleteFileFormat(fileFormat).build();
     EncryptedOutputFile outputFile = 
table.encryption().encrypt(HadoopOutputFile.fromPath(
         new org.apache.hadoop.fs.Path(table.location(), deleteFilePath), new 
Configuration()));
 
@@ -407,13 +411,15 @@ public static DeleteFile createPositionalDeleteFile(Table 
table, String deleteFi
       partitionKey.partition(record);
     }
 
-    PositionDeleteWriter<Record> posWriter = 
appenderFactory.newPosDeleteWriter(outputFile, fileFormat, partitionKey);
-    try (PositionDeleteWriter<Record> writer = posWriter) {
-      deletes.forEach(del -> {
-        PositionDelete positionDelete = PositionDelete.create();
-        positionDelete.set(del.path(), del.pos(), del.row());
-        writer.write(positionDelete);
-      });
+    PositionDeleteWriter<Record> posWriter =
+        writerFactory.newPositionDeleteWriter(outputFile, table.spec(), 
partitionKey);
+    try (posWriter) {
+      deletes.forEach(
+          del -> {
+            PositionDelete positionDelete = PositionDelete.create();
+            positionDelete.set(del.path(), del.pos());
+            posWriter.write(positionDelete);
+          });
     }
     return posWriter.toDeleteFile();
   }
diff --git 
a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/DVUtil.java 
b/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/DVUtil.java
deleted file mode 100644
index 5f67d57e540..00000000000
--- a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/DVUtil.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.iceberg;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ExecutorService;
-import org.apache.iceberg.deletes.DVFileWriter;
-import org.apache.iceberg.deletes.Deletes;
-import org.apache.iceberg.deletes.PositionDeleteIndex;
-import org.apache.iceberg.encryption.EncryptedFiles;
-import org.apache.iceberg.encryption.EncryptedOutputFile;
-import org.apache.iceberg.encryption.EncryptingFileIO;
-import org.apache.iceberg.io.FileIO;
-import org.apache.iceberg.io.IOUtil;
-import org.apache.iceberg.io.InputFile;
-import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
-import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
-import org.apache.iceberg.relocated.com.google.common.collect.Lists;
-import org.apache.iceberg.relocated.com.google.common.collect.Maps;
-import org.apache.iceberg.relocated.com.google.common.collect.Multimap;
-import org.apache.iceberg.relocated.com.google.common.collect.Multimaps;
-import org.apache.iceberg.types.Comparators;
-import org.apache.iceberg.util.ContentFileUtil;
-import org.apache.iceberg.util.Pair;
-import org.apache.iceberg.util.Tasks;
-
-class DVUtil {
-  private DVUtil() {
-  }
-
-  static PositionDeleteIndex readDV(DeleteFile deleteFile, FileIO fileIO) {
-    Preconditions.checkArgument(
-        ContentFileUtil.isDV(deleteFile),
-        "Cannot read, not a deletion vector: %s",
-        deleteFile.location());
-    InputFile inputFile = fileIO.newInputFile(deleteFile);
-    long offset = deleteFile.contentOffset();
-    int length = deleteFile.contentSizeInBytes().intValue();
-    byte[] bytes = new byte[length];
-    try {
-      IOUtil.readFully(inputFile, offset, bytes, 0, length);
-      return PositionDeleteIndex.deserialize(bytes, deleteFile);
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-
-  /**
-   * Merges duplicate DVs for the same data file and writes the merged DV 
Puffin files. If there is
-   * exactly 1 DV for a given data file then it is return as is
-   *
-   * @param dvsByReferencedFile map of data file location to DVs
-   * @param mergedOutputLocation output location of the merged DVs
-   * @param fileIO fileIO to use when reading and writing
-   * @param specs partition specs
-   * @param pool executor service for reading DVs
-   * @return a list containing both any newly merged DVs and any DVs that are 
already valid
-   */
-  static List<DeleteFile> mergeAndWriteDVsIfRequired(
-      Map<String, List<DeleteFile>> dvsByReferencedFile,
-      String mergedOutputLocation,
-      FileIO fileIO,
-      Map<Integer, PartitionSpec> specs,
-      ExecutorService pool) {
-    List<DeleteFile> finalDVs = Lists.newArrayList();
-    Multimap<String, DeleteFile> duplicates =
-        Multimaps.newListMultimap(Maps.newHashMap(), Lists::newArrayList);
-    Map<String, Pair<PartitionSpec, StructLike>> partitions = 
Maps.newHashMap();
-
-    for (Map.Entry<String, List<DeleteFile>> entry : 
dvsByReferencedFile.entrySet()) {
-      if (entry.getValue().size() > 1) {
-        duplicates.putAll(entry.getKey(), entry.getValue());
-        DeleteFile first = entry.getValue().get(0);
-        partitions.put(entry.getKey(), Pair.of(specs.get(first.specId()), 
first.partition()));
-      } else {
-        finalDVs.addAll(entry.getValue());
-      }
-    }
-
-    if (duplicates.isEmpty()) {
-      return finalDVs;
-    }
-
-    validateCanMerge(duplicates, partitions);
-
-    Map<String, PositionDeleteIndex> deletes =
-        readAndMergeDVs(duplicates.values().toArray(DeleteFile[]::new), 
fileIO, pool);
-
-    finalDVs.addAll(writeDVs(deletes, fileIO, mergedOutputLocation, 
partitions));
-    return finalDVs;
-  }
-
-  private static void validateCanMerge(
-      Multimap<String, DeleteFile> duplicates,
-      Map<String, Pair<PartitionSpec, StructLike>> partitions) {
-    Map<Integer, Comparator<StructLike>> comparatorsBySpecId = 
Maps.newHashMap();
-    for (Map.Entry<String, Collection<DeleteFile>> entry : 
duplicates.asMap().entrySet()) {
-      String referencedFile = entry.getKey();
-
-      // validate that each file matches the expected partition
-      Pair<PartitionSpec, StructLike> partition = 
partitions.get(referencedFile);
-      Long sequenceNumber = Iterables.getFirst(entry.getValue(), 
null).dataSequenceNumber();
-      PartitionSpec spec = partition.first();
-      StructLike tuple = partition.second();
-      Comparator<StructLike> comparator =
-          comparatorsBySpecId.computeIfAbsent(
-              spec.specId(), id -> Comparators.forType(spec.partitionType()));
-
-      for (DeleteFile dv : entry.getValue()) {
-        Preconditions.checkArgument(
-            Objects.equals(sequenceNumber, dv.dataSequenceNumber()),
-            "Cannot merge DVs, mismatched sequence numbers (%s, %s) for %s",
-            sequenceNumber,
-            dv.dataSequenceNumber(),
-            referencedFile);
-
-        Preconditions.checkArgument(
-            spec.specId() == dv.specId(),
-            "Cannot merge DVs, mismatched partition specs (%s, %s) for %s",
-            spec.specId(),
-            dv.specId(),
-            referencedFile);
-
-        Preconditions.checkArgument(
-            comparator.compare(tuple, dv.partition()) == 0,
-            "Cannot merge DVs, mismatched partition tuples (%s, %s) for %s",
-            tuple,
-            dv.partition(),
-            referencedFile);
-      }
-    }
-  }
-
-  /**
-   * Reads all DVs, and merge the position indices per referenced data file
-   *
-   * @param duplicateDVs list of dvs to read and merge
-   * @param io the FileIO to use for reading DV files
-   * @param pool executor service for reading DVs
-   * @return map of referenced data file location to the merged position 
delete index
-   */
-  private static Map<String, PositionDeleteIndex> readAndMergeDVs(
-      DeleteFile[] duplicateDVs, FileIO io, ExecutorService pool) {
-    // Read all duplicate DVs in parallel
-    PositionDeleteIndex[] duplicatedDVPositions = new 
PositionDeleteIndex[duplicateDVs.length];
-    Tasks.range(duplicatedDVPositions.length)
-        .executeWith(pool)
-        .stopOnFailure()
-        .throwFailureWhenFinished()
-        .run(i -> duplicatedDVPositions[i] = readDV(duplicateDVs[i], io));
-
-    Map<String, PositionDeleteIndex> mergedDVs = Maps.newHashMap();
-    for (int i = 0; i < duplicatedDVPositions.length; i++) {
-      DeleteFile dv = duplicateDVs[i];
-      PositionDeleteIndex previousDV = 
mergedDVs.get(duplicateDVs[i].referencedDataFile());
-      if (previousDV != null) {
-        previousDV.merge(duplicatedDVPositions[i]);
-      } else {
-        mergedDVs.put(dv.referencedDataFile(), duplicatedDVPositions[i]);
-      }
-    }
-
-    return mergedDVs;
-  }
-
-  // Produces a single Puffin file containing the merged DVs
-  private static List<DeleteFile> writeDVs(
-      Map<String, PositionDeleteIndex> mergedIndexByFile,
-      FileIO fileIO,
-      String dvOutputLocation,
-      Map<String, Pair<PartitionSpec, StructLike>> partitions) {
-    EncryptedOutputFile dvOutputFile =
-        fileIO instanceof EncryptingFileIO encryptingFileIO ?
-            encryptingFileIO.newEncryptingOutputFile(dvOutputLocation) :
-            
EncryptedFiles.plainAsEncryptedOutput(fileIO.newOutputFile(dvOutputLocation));
-
-    try (DVFileWriter dvFileWriter = Deletes.writeDVs(dvOutputFile, path -> 
null)) {
-      for (Map.Entry<String, PositionDeleteIndex> entry : 
mergedIndexByFile.entrySet()) {
-        String referencedLocation = entry.getKey();
-        PositionDeleteIndex mergedPositions = entry.getValue();
-        Pair<PartitionSpec, StructLike> partition = 
partitions.get(referencedLocation);
-        dvFileWriter.delete(
-            referencedLocation, mergedPositions, partition.first(), 
partition.second());
-      }
-      dvFileWriter.close();
-      return dvFileWriter.result().deleteFiles();
-    } catch (IOException e) {
-      throw new UncheckedIOException(e);
-    }
-  }
-}
diff --git 
a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/BaseDVFileWriter.java
 
b/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/BaseDVFileWriter.java
deleted file mode 100644
index ab8c85b50eb..00000000000
--- 
a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/BaseDVFileWriter.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.iceberg.deletes;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import org.apache.iceberg.DeleteFile;
-import org.apache.iceberg.FileFormat;
-import org.apache.iceberg.FileMetadata;
-import org.apache.iceberg.IcebergBuild;
-import org.apache.iceberg.MetadataColumns;
-import org.apache.iceberg.PartitionSpec;
-import org.apache.iceberg.StructLike;
-import org.apache.iceberg.encryption.EncryptedFiles;
-import org.apache.iceberg.encryption.EncryptedOutputFile;
-import org.apache.iceberg.encryption.EncryptionKeyMetadata;
-import org.apache.iceberg.encryption.NativeEncryptionKeyMetadata;
-import org.apache.iceberg.io.DeleteWriteResult;
-import org.apache.iceberg.io.OutputFile;
-import org.apache.iceberg.io.OutputFileFactory;
-import org.apache.iceberg.puffin.Blob;
-import org.apache.iceberg.puffin.BlobMetadata;
-import org.apache.iceberg.puffin.Puffin;
-import org.apache.iceberg.puffin.PuffinWriter;
-import org.apache.iceberg.puffin.StandardBlobTypes;
-import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
-import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
-import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
-import org.apache.iceberg.relocated.com.google.common.collect.Lists;
-import org.apache.iceberg.relocated.com.google.common.collect.Maps;
-import org.apache.iceberg.util.CharSequenceSet;
-import org.apache.iceberg.util.ContentFileUtil;
-import org.apache.iceberg.util.StructLikeUtil;
-
-public class BaseDVFileWriter implements DVFileWriter {
-
-  private static final String REFERENCED_DATA_FILE_KEY = 
"referenced-data-file";
-  private static final String CARDINALITY_KEY = "cardinality";
-
-  private final Supplier<EncryptedOutputFile> dvOutputFile;
-  private final Function<String, PositionDeleteIndex> loadPreviousDeletes;
-  private final Map<String, Deletes> deletesByPath = Maps.newHashMap();
-  private final Map<String, BlobMetadata> blobsByPath = Maps.newHashMap();
-  private DeleteWriteResult result = null;
-
-  public BaseDVFileWriter(
-      OutputFileFactory fileFactory, Function<String, PositionDeleteIndex> 
loadPreviousDeletes) {
-    this(loadPreviousDeletes, fileFactory::newOutputFile);
-  }
-
-  public BaseDVFileWriter(
-      Supplier<OutputFile> dvOutputFile,
-      Function<String, PositionDeleteIndex> loadPreviousDeletes) {
-    this(loadPreviousDeletes, () -> 
EncryptedFiles.plainAsEncryptedOutput(dvOutputFile.get()));
-  }
-
-  BaseDVFileWriter(
-      Function<String, PositionDeleteIndex> loadPreviousDeletes,
-      Supplier<EncryptedOutputFile> dvOutputFile) {
-    this.dvOutputFile = dvOutputFile;
-    this.loadPreviousDeletes = loadPreviousDeletes;
-  }
-
-  @Override
-  public void delete(String path, long pos, PartitionSpec spec, StructLike 
partition) {
-    Deletes deletes =
-        deletesByPath.computeIfAbsent(path, key -> new Deletes(path, spec, 
partition));
-    PositionDeleteIndex positions = deletes.positions();
-    positions.delete(pos);
-  }
-
-  @Override
-  public void delete(
-      String path,
-      PositionDeleteIndex positionDeleteIndex,
-      PartitionSpec spec,
-      StructLike partition) {
-    Deletes deletes =
-        deletesByPath.computeIfAbsent(path, key -> new Deletes(path, spec, 
partition));
-    deletes.positions().merge(positionDeleteIndex);
-  }
-
-  @Override
-  public DeleteWriteResult result() {
-    Preconditions.checkState(result != null, "Cannot get result from unclosed 
writer");
-    return result;
-  }
-
-  @Override
-  public void close() throws IOException {
-    if (result == null) {
-      List<DeleteFile> dvs = Lists.newArrayList();
-      CharSequenceSet referencedDataFiles = CharSequenceSet.empty();
-      List<DeleteFile> rewrittenDeleteFiles = Lists.newArrayList();
-
-      // Only create PuffinWriter if there are deletes to write
-      if (deletesByPath.isEmpty()) {
-        this.result = new DeleteWriteResult(dvs, referencedDataFiles, 
rewrittenDeleteFiles);
-        return;
-      }
-
-      EncryptedOutputFile outputFile = dvOutputFile.get();
-      EncryptionKeyMetadata keyMetadata = outputFile.keyMetadata();
-      PuffinWriter writer =
-          Puffin.write(outputFile.encryptingOutputFile())
-              .createdBy(IcebergBuild.fullVersion())
-              .build();
-
-      try (PuffinWriter closeableWriter = writer) {
-        for (Deletes deletes : deletesByPath.values()) {
-          String path = deletes.path();
-          PositionDeleteIndex positions = deletes.positions();
-          PositionDeleteIndex previousPositions = 
loadPreviousDeletes.apply(path);
-          if (previousPositions != null) {
-            positions.merge(previousPositions);
-            for (DeleteFile previousDeleteFile : 
previousPositions.deleteFiles()) {
-              // only DVs and file-scoped deletes can be discarded from the 
table state
-              if (ContentFileUtil.isFileScoped(previousDeleteFile)) {
-                rewrittenDeleteFiles.add(previousDeleteFile);
-              }
-            }
-          }
-          write(closeableWriter, deletes);
-          referencedDataFiles.add(path);
-        }
-      }
-
-      // DVs share the Puffin path and file size but have different offsets
-      String puffinPath = writer.location();
-      long puffinFileSize = writer.fileSize();
-
-      for (String path : deletesByPath.keySet()) {
-        DeleteFile dv = createDV(puffinPath, puffinFileSize, path, 
keyMetadata);
-        dvs.add(dv);
-      }
-
-      this.result = new DeleteWriteResult(dvs, referencedDataFiles, 
rewrittenDeleteFiles);
-    }
-  }
-
-  private DeleteFile createDV(
-      String path, long size, String referencedDataFile, EncryptionKeyMetadata 
keyMetadata) {
-    Deletes deletes = deletesByPath.get(referencedDataFile);
-    BlobMetadata blobMetadata = blobsByPath.get(referencedDataFile);
-    return FileMetadata.deleteFileBuilder(deletes.spec())
-        .ofPositionDeletes()
-        .withFormat(FileFormat.PUFFIN)
-        .withPath(path)
-        .withPartition(deletes.partition())
-        .withFileSizeInBytes(size)
-        .withEncryptionKeyMetadata(encryptionKeyMetadata(size, keyMetadata))
-        .withReferencedDataFile(referencedDataFile)
-        .withContentOffset(blobMetadata.offset())
-        .withContentSizeInBytes(blobMetadata.length())
-        .withRecordCount(deletes.positions().cardinality())
-        .build();
-  }
-
-  private void write(PuffinWriter writer, Deletes deletes) {
-    String path = deletes.path();
-    PositionDeleteIndex positions = deletes.positions();
-    BlobMetadata blobMetadata = writer.write(toBlob(positions, path));
-    blobsByPath.put(path, blobMetadata);
-  }
-
-  private ByteBuffer encryptionKeyMetadata(
-      long fileSizeInBytes, EncryptionKeyMetadata keyMetadata) {
-    if (keyMetadata instanceof NativeEncryptionKeyMetadata nativeKeyMetadata) {
-      return nativeKeyMetadata.copyWithLength(fileSizeInBytes).buffer();
-    }
-
-    return keyMetadata.buffer();
-  }
-
-  private Blob toBlob(PositionDeleteIndex positions, String path) {
-    return new Blob(
-        StandardBlobTypes.DV_V1,
-        ImmutableList.of(MetadataColumns.ROW_POSITION.fieldId()),
-        -1 /* snapshot ID is inherited */,
-        -1 /* sequence number is inherited */,
-        positions.serialize(),
-        null /* uncompressed */,
-        ImmutableMap.of(
-            REFERENCED_DATA_FILE_KEY,
-            path,
-            CARDINALITY_KEY,
-            String.valueOf(positions.cardinality())));
-  }
-
-  private static class Deletes {
-    private final String path;
-    private final PositionDeleteIndex positions;
-    private final PartitionSpec spec;
-    private final StructLike partition;
-
-    private Deletes(String path, PartitionSpec spec, StructLike partition) {
-      this.path = path;
-      this.positions = new BitmapPositionDeleteIndex();
-      this.spec = spec;
-      this.partition = StructLikeUtil.copy(partition);
-    }
-
-    public String path() {
-      return path;
-    }
-
-    public PositionDeleteIndex positions() {
-      return positions;
-    }
-
-    public PartitionSpec spec() {
-      return spec;
-    }
-
-    public StructLike partition() {
-      return partition;
-    }
-  }
-}
diff --git 
a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/Deletes.java
 
b/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/Deletes.java
deleted file mode 100644
index 127c8046f9e..00000000000
--- 
a/iceberg/patched-iceberg-core/src/main/java/org/apache/iceberg/deletes/Deletes.java
+++ /dev/null
@@ -1,242 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.iceberg.deletes;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import org.apache.iceberg.Accessor;
-import org.apache.iceberg.DeleteFile;
-import org.apache.iceberg.MetadataColumns;
-import org.apache.iceberg.Schema;
-import org.apache.iceberg.StructLike;
-import org.apache.iceberg.encryption.EncryptedOutputFile;
-import org.apache.iceberg.io.CloseableIterable;
-import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
-import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
-import org.apache.iceberg.relocated.com.google.common.collect.Lists;
-import org.apache.iceberg.types.Comparators;
-import org.apache.iceberg.types.Types;
-import org.apache.iceberg.util.CharSequenceMap;
-import org.apache.iceberg.util.Filter;
-import org.apache.iceberg.util.SortedMerge;
-import org.apache.iceberg.util.StructLikeSet;
-
-public class Deletes {
-
-  private static final Schema POSITION_DELETE_SCHEMA =
-      new Schema(MetadataColumns.DELETE_FILE_PATH, 
MetadataColumns.DELETE_FILE_POS);
-
-  private static final Accessor<StructLike> FILENAME_ACCESSOR =
-      
POSITION_DELETE_SCHEMA.accessorForField(MetadataColumns.DELETE_FILE_PATH.fieldId());
-  private static final Accessor<StructLike> POSITION_ACCESSOR =
-      
POSITION_DELETE_SCHEMA.accessorForField(MetadataColumns.DELETE_FILE_POS.fieldId());
-
-  private Deletes() {
-  }
-
-  public static DVFileWriter writeDVs(
-      EncryptedOutputFile outputFile, Function<String, PositionDeleteIndex> 
loadPreviousDeletes) {
-    return new BaseDVFileWriter(loadPreviousDeletes, () -> outputFile);
-  }
-
-  public static <T> CloseableIterable<T> filter(
-      CloseableIterable<T> rows, Function<T, StructLike> rowToDeleteKey, 
StructLikeSet deleteSet) {
-    if (deleteSet.isEmpty()) {
-      return rows;
-    }
-
-    EqualitySetDeleteFilter<T> equalityFilter =
-        new EqualitySetDeleteFilter<>(rowToDeleteKey, deleteSet);
-    return equalityFilter.filter(rows);
-  }
-
-  /**
-   * Returns the same rows that are input, while marking the deleted ones.
-   *
-   * @param rows the rows to process
-   * @param isDeleted a predicate that determines if a row is deleted
-   * @param deleteMarker a function that marks a row as deleted
-   * @return the processed rows
-   */
-  public static <T> CloseableIterable<T> markDeleted(
-      CloseableIterable<T> rows, Predicate<T> isDeleted, Consumer<T> 
deleteMarker) {
-    return CloseableIterable.transform(
-        rows,
-        row -> {
-          if (isDeleted.test(row)) {
-            deleteMarker.accept(row);
-          }
-
-          return row;
-        });
-  }
-
-  /**
-   * Returns the remaining rows (the ones that are not deleted), while 
counting the deleted ones.
-   *
-   * @param rows the rows to process
-   * @param isDeleted a predicate that determines if a row is deleted
-   * @param counter a counter that counts deleted rows
-   * @return the processed rows
-   */
-  public static <T> CloseableIterable<T> filterDeleted(
-      CloseableIterable<T> rows, Predicate<T> isDeleted, DeleteCounter 
counter) {
-    Filter<T> remainingRowsFilter =
-        new Filter<T>() {
-          @Override
-          protected boolean shouldKeep(T item) {
-            boolean deleted = isDeleted.test(item);
-            if (deleted) {
-              counter.increment();
-            }
-
-            return !deleted;
-          }
-        };
-
-    return remainingRowsFilter.filter(rows);
-  }
-
-  public static StructLikeSet toEqualitySet(
-      CloseableIterable<StructLike> eqDeletes, Types.StructType eqType) {
-    try (CloseableIterable<StructLike> deletes = eqDeletes) {
-      StructLikeSet deleteSet = StructLikeSet.create(eqType);
-      Iterables.addAll(deleteSet, deletes);
-      return deleteSet;
-    } catch (IOException e) {
-      throw new UncheckedIOException("Failed to close equality delete source", 
e);
-    }
-  }
-
-  public static <T extends StructLike> CharSequenceMap<PositionDeleteIndex> 
toPositionIndexes(
-      CloseableIterable<T> posDeletes) {
-    return toPositionIndexes(posDeletes, null /* unknown delete file */);
-  }
-
-  /**
-   * Builds a map of position delete indexes by path.
-   *
-   * <p>This method builds a position delete index for each referenced data 
file and does not filter
-   * deletes. This can be useful when the entire delete file content is needed 
(e.g. caching).
-   *
-   * @param posDeletes position deletes
-   * @param file the source delete file for the deletes
-   * @return the map of position delete indexes by path
-   */
-  public static <T extends StructLike> CharSequenceMap<PositionDeleteIndex> 
toPositionIndexes(
-      CloseableIterable<T> posDeletes, DeleteFile file) {
-    CharSequenceMap<PositionDeleteIndex> indexes = CharSequenceMap.create();
-
-    try (CloseableIterable<T> deletes = posDeletes) {
-      for (T delete : deletes) {
-        CharSequence filePath = (CharSequence) FILENAME_ACCESSOR.get(delete);
-        long position = (long) POSITION_ACCESSOR.get(delete);
-        PositionDeleteIndex index =
-            indexes.computeIfAbsent(filePath, key -> new 
BitmapPositionDeleteIndex(file));
-        index.delete(position);
-      }
-    } catch (IOException e) {
-      throw new UncheckedIOException("Failed to close position delete source", 
e);
-    }
-
-    return indexes;
-  }
-
-  public static <T extends StructLike> PositionDeleteIndex toPositionIndex(
-      CharSequence dataLocation, CloseableIterable<T> posDeletes, DeleteFile 
file) {
-    CloseableIterable<Long> positions = extractPositions(dataLocation, 
posDeletes);
-    List<DeleteFile> files = ImmutableList.of(file);
-    return toPositionIndex(positions, files);
-  }
-
-  private static <T extends StructLike> CloseableIterable<Long> 
extractPositions(
-      CharSequence dataLocation, CloseableIterable<T> rows) {
-    DataFileFilter<T> filter = new DataFileFilter<>(dataLocation);
-    CloseableIterable<T> filteredRows = filter.filter(rows);
-    return CloseableIterable.transform(filteredRows, row -> (Long) 
POSITION_ACCESSOR.get(row));
-  }
-
-  public static PositionDeleteIndex toPositionIndex(CloseableIterable<Long> 
posDeletes) {
-    return toPositionIndex(posDeletes, ImmutableList.of());
-  }
-
-  private static PositionDeleteIndex toPositionIndex(
-      CloseableIterable<Long> posDeletes, List<DeleteFile> files) {
-    try (CloseableIterable<Long> deletes = posDeletes) {
-      PositionDeleteIndex positionDeleteIndex = new 
BitmapPositionDeleteIndex(files);
-      deletes.forEach(positionDeleteIndex::delete);
-      return positionDeleteIndex;
-    } catch (IOException e) {
-      throw new UncheckedIOException("Failed to close position delete source", 
e);
-    }
-  }
-
-  public static CloseableIterable<Long> deletePositions(
-      CharSequence dataLocation, CloseableIterable<StructLike> deleteFile) {
-    return deletePositions(dataLocation, ImmutableList.of(deleteFile));
-  }
-
-  public static <T extends StructLike> CloseableIterable<Long> deletePositions(
-      CharSequence dataLocation, List<CloseableIterable<T>> deleteFiles) {
-    DataFileFilter<T> locationFilter = new DataFileFilter<>(dataLocation);
-    List<CloseableIterable<Long>> positions =
-        Lists.transform(
-            deleteFiles,
-            deletes ->
-                CloseableIterable.transform(
-                    locationFilter.filter(deletes), row -> (Long) 
POSITION_ACCESSOR.get(row)));
-
-    return new SortedMerge<>(Long::compare, positions);
-  }
-
-  private static class EqualitySetDeleteFilter<T> extends Filter<T> {
-    private final StructLikeSet deletes;
-    private final Function<T, StructLike> extractEqStruct;
-
-    protected EqualitySetDeleteFilter(Function<T, StructLike> extractEq, 
StructLikeSet deletes) {
-      this.extractEqStruct = extractEq;
-      this.deletes = deletes;
-    }
-
-    @Override
-    protected boolean shouldKeep(T row) {
-      return !deletes.contains(extractEqStruct.apply(row));
-    }
-  }
-
-  private static class DataFileFilter<T extends StructLike> extends Filter<T> {
-    private final CharSequence dataLocation;
-
-    DataFileFilter(CharSequence dataLocation) {
-      this.dataLocation = dataLocation;
-    }
-
-    @Override
-    protected boolean shouldKeep(T posDelete) {
-      return Comparators.filePath()
-              .compare(dataLocation, (CharSequence) 
FILENAME_ACCESSOR.get(posDelete)) ==
-          0;
-    }
-  }
-}
diff --git a/iceberg/pom.xml b/iceberg/pom.xml
index 7fcb6387e4f..543436192d2 100644
--- a/iceberg/pom.xml
+++ b/iceberg/pom.xml
@@ -41,7 +41,7 @@
     <assertj.version>3.27.3</assertj.version>
     <immutables.value.version>2.10.0</immutables.value.version>
     <validate.skip>false</validate.skip>
-    <caffeine.version>2.9.3</caffeine.version>
+    <caffeine.version>3.2.4</caffeine.version>
   </properties>
   <modules>
     <module>patched-iceberg-api</module>
diff --git a/itests/qtest-iceberg/pom.xml b/itests/qtest-iceberg/pom.xml
index 23e68ce4e01..5243eaa64fd 100644
--- a/itests/qtest-iceberg/pom.xml
+++ b/itests/qtest-iceberg/pom.xml
@@ -558,6 +558,11 @@
       <artifactId>jaxb-runtime</artifactId>
       <version>${jaxb-runtime.version}</version>
     </dependency>
+    <dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>apache5-client</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git a/pom.xml b/pom.xml
index a887c6333b8..e774827e5c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -159,7 +159,7 @@
     <httpcomponents.client.version>4.5.13</httpcomponents.client.version>
     <httpcomponents.core.version>4.4.13</httpcomponents.core.version>
     <!-- Upgrade roaringbit version whenever upgrading iceberg version -->
-    <iceberg.version>1.11.0</iceberg.version>
+    <iceberg.version>1.12.0</iceberg.version>
     <immutables.version>2.9.2</immutables.version>
     <httpcomponents5.core.version>5.3.4</httpcomponents5.core.version>
     <httpcomponents5.client.version>5.5</httpcomponents5.client.version>
@@ -211,7 +211,7 @@
     <protoc-jar-maven-plugin.version>3.11.4</protoc-jar-maven-plugin.version>
     <protoc.path>${env.PROTOC_PATH}</protoc.path>
     <rat.version>0.16.1</rat.version>
-    <roaringbit.version>1.3.0</roaringbit.version>
+    <roaringbit.version>1.6.23</roaringbit.version>
     <stax.version>1.0.1</stax.version>
     <slf4j.version>1.7.30</slf4j.version>
     <ST4.version>4.0.4</ST4.version>
@@ -288,6 +288,11 @@
         <version>${aws-java-sdk.version}</version>
         <scope>provided</scope>
       </dependency>
+      <dependency>
+        <groupId>software.amazon.awssdk</groupId>
+        <artifactId>apache5-client</artifactId>
+        <version>${aws-java-sdk.version}</version>
+      </dependency>
       <dependency>
         <groupId>com.amazonaws.secretsmanager</groupId>
         <artifactId>aws-secretsmanager-caching-java</artifactId>
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
index 859a8ebe601..6357fb4b080 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
@@ -26,6 +26,7 @@
 import java.time.Clock;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.iceberg.BaseTable;
@@ -378,7 +379,12 @@ private RESTResponse tableExists(Map<String, String> vars) 
{
 
   private LoadTableResponse loadTable(Map<String, String> vars) {
     TableIdentifier ident = identFromPathVars(vars);
-    return CatalogHandlers.loadTable(catalog, ident);
+    String snapshots = 
vars.get(RESTCatalogProperties.SNAPSHOTS_QUERY_PARAMETER);
+    RESTCatalogProperties.SnapshotMode mode = 
RESTCatalogProperties.SNAPSHOT_LOADING_MODE_DEFAULT;
+    if (snapshots != null) {
+      mode = 
RESTCatalogProperties.SnapshotMode.valueOf(snapshots.toUpperCase(Locale.ROOT));
+    }
+    return CatalogHandlers.loadTable(catalog, ident, mode);
   }
 
   private LoadTableResponse registerTable(Map<String, String> vars, Object 
body) {
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterSnapshotMode.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterSnapshotMode.java
new file mode 100644
index 00000000000..fdd0ad5773a
--- /dev/null
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestHMSCatalogAdapterSnapshotMode.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iceberg.rest;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.iceberg.BaseTable;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.SnapshotRef;
+import org.apache.iceberg.SortOrder;
+import org.apache.iceberg.TableMetadata;
+import org.apache.iceberg.TableOperations;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.SupportsNamespaces;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.catalog.ViewCatalog;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
+import org.apache.iceberg.rest.responses.LoadTableResponse;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestHMSCatalogAdapterSnapshotMode {
+
+  private static final String TABLE_PATH = 
"v1/namespaces/accounting/tables/my_table";
+
+  private HMSCatalogAdapter adapter;
+  private HttpServletResponse response;
+  private StringWriter stringWriter;
+
+  @BeforeEach
+  void setup() throws Exception {
+    Catalog catalog =
+        Mockito.mock(
+            Catalog.class,
+            Mockito.withSettings().extraInterfaces(SupportsNamespaces.class, 
ViewCatalog.class));
+
+    Schema schema = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+
+    Snapshot snap1 = Mockito.mock(Snapshot.class);
+    Mockito.when(snap1.snapshotId()).thenReturn(1L);
+    Mockito.when(snap1.sequenceNumber()).thenReturn(1L);
+    Mockito.when(snap1.timestampMillis()).thenReturn(1000L);
+
+    Snapshot snap2 = Mockito.mock(Snapshot.class);
+    Mockito.when(snap2.snapshotId()).thenReturn(2L);
+    Mockito.when(snap2.sequenceNumber()).thenReturn(2L);
+    Mockito.when(snap2.timestampMillis()).thenReturn(2000L);
+
+    TableMetadata initialMetadata =
+        TableMetadata.buildFromEmpty(2)
+            .setLocation("file:///tmp/table")
+            .addSchema(schema)
+            .setDefaultPartitionSpec(PartitionSpec.unpartitioned())
+            .setDefaultSortOrder(SortOrder.unsorted())
+            .addSnapshot(snap1)
+            .addSnapshot(snap2)
+            .setBranchSnapshot(2L, SnapshotRef.MAIN_BRANCH)
+            .discardChanges()
+            .withMetadataLocation("file:///tmp/table/metadata.json")
+            .build();
+
+    BaseTable table = Mockito.mock(BaseTable.class);
+    TableOperations operations = Mockito.mock(TableOperations.class);
+    Mockito.when(table.operations()).thenReturn(operations);
+    Mockito.when(operations.current()).thenReturn(initialMetadata);
+    
Mockito.when(catalog.loadTable(Mockito.any(TableIdentifier.class))).thenReturn(table);
+
+    adapter = new HMSCatalogAdapter("test_catalog", catalog, null, 
Collections.emptyList());
+
+    response = Mockito.mock(HttpServletResponse.class);
+    stringWriter = new StringWriter();
+    Mockito.when(response.getWriter()).thenReturn(new 
PrintWriter(stringWriter));
+  }
+
+  @AfterEach
+  void tearDown() {
+    if (adapter != null) {
+      adapter.close();
+    }
+  }
+
+  private LoadTableResponse executeLoadTable(String snapshotMode) throws 
Exception {
+    Map<String, String> queryParams =
+        snapshotMode != null
+            ? ImmutableMap.of(RESTCatalogProperties.SNAPSHOTS_QUERY_PARAMETER, 
snapshotMode)
+            : null;
+    return adapter.execute(HTTPMethod.GET, TABLE_PATH, queryParams, null, 
response);
+  }
+
+  @Test
+  void testDefaultSnapshotMode() throws Exception {
+    // When snapshots query parameter is omitted, mode defaults to 
SnapshotMode.ALL
+    LoadTableResponse res = executeLoadTable(null);
+
+    Assertions.assertNotNull(res, "Response should not be null");
+    Assertions.assertNotNull(res.tableMetadata(), "TableMetadata should not be 
null");
+    Assertions.assertEquals(
+        2,
+        res.tableMetadata().snapshots().size(),
+        "Default mode (ALL) should preserve all snapshots including historical 
ones");
+    Assertions.assertNotNull(res.tableMetadata().snapshot(1L), "Snapshot 1 
should be retained");
+    Assertions.assertNotNull(res.tableMetadata().snapshot(2L), "Snapshot 2 
should be retained");
+  }
+
+  @Test
+  void testSnapshotModeAll() throws Exception {
+    LoadTableResponse res = executeLoadTable("all");
+
+    Assertions.assertNotNull(res, "Response should not be null");
+    Assertions.assertNotNull(res.tableMetadata(), "TableMetadata should not be 
null");
+    Assertions.assertEquals(
+        2, res.tableMetadata().snapshots().size(), "Mode ALL should return all 
snapshots");
+    Assertions.assertNotNull(
+        res.tableMetadata().snapshot(1L), "Historical snapshot 1 should be 
present");
+    Assertions.assertNotNull(
+        res.tableMetadata().snapshot(2L), "Branch snapshot 2 should be 
present");
+  }
+
+  @Test
+  void testSnapshotModeRefs() throws Exception {
+    LoadTableResponse res = executeLoadTable("refs");
+
+    Assertions.assertNotNull(res, "Response should not be null");
+    Assertions.assertNotNull(res.tableMetadata(), "TableMetadata should not be 
null");
+    Assertions.assertEquals(
+        1,
+        res.tableMetadata().snapshots().size(),
+        "Mode REFS should suppress historical snapshots");
+    Assertions.assertNull(
+        res.tableMetadata().snapshot(1L),
+        "Historical snapshot 1 should be suppressed in REFS mode");
+    Assertions.assertNotNull(
+        res.tableMetadata().snapshot(2L), "Branch snapshot 2 should be 
retained in REFS mode");
+  }
+
+  @Test
+  void testInvalidSnapshotMode() throws Exception {
+    LoadTableResponse res = executeLoadTable("invalid");
+
+    Assertions.assertNull(res, "Response should be null for an invalid 
snapshot mode");
+    Mockito.verify(response).setStatus(400);
+    Assertions.assertTrue(
+        stringWriter.toString().contains("IllegalArgumentException"),
+        "Error response should report IllegalArgumentException");
+    Assertions.assertTrue(
+        stringWriter.toString().contains("INVALID"),
+        "Error response message should mention the invalid mode");
+  }
+}
diff --git a/standalone-metastore/pom.xml b/standalone-metastore/pom.xml
index f532004b138..bc0e48099ed 100644
--- a/standalone-metastore/pom.xml
+++ b/standalone-metastore/pom.xml
@@ -93,7 +93,7 @@
     <guava.version>22.0</guava.version>
     <hadoop.version>3.4.2</hadoop.version>
     <hikaricp.version>4.0.3</hikaricp.version>
-    <iceberg.version>1.11.0</iceberg.version>
+    <iceberg.version>1.12.0</iceberg.version>
     <jexl.version>3.7.0</jexl.version>
     <jackson.version>2.21.5</jackson.version>
     <javolution.version>5.5.1</javolution.version>

Reply via email to