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

okumin 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 4486ba4a3ba HIVE-29788: Support for rewrite_manifests iceberg 
procedure (#6667)
4486ba4a3ba is described below

commit 4486ba4a3bae1cd4b1c4856710c1967ef4de7fd0
Author: Raghav Aggarwal <[email protected]>
AuthorDate: Wed Aug 12 14:50:07 2026 +0530

    HIVE-29788: Support for rewrite_manifests iceberg procedure (#6667)
    
    * HIVE-29788: Support for rewrite_manifests iceberg procedure
    
    * Review comments
    
    The current implementation targets only data manifests.
---
 .../iceberg/mr/hive/HiveIcebergStorageHandler.java |   3 +
 .../apache/iceberg/mr/hive/IcebergTableUtil.java   | 111 +++++++++++++++++++
 .../mr/hive/TestHiveIcebergSnapshotOperations.java | 119 +++++++++++++++++++++
 .../hadoop/hive/ql/parse/AlterClauseParser.g       |   2 +
 .../apache/hadoop/hive/ql/parse/HiveLexerParent.g  |   1 +
 .../hadoop/hive/ql/parse/IdentifiersParser.g       |   1 +
 .../table/execute/AlterTableExecuteAnalyzer.java   |   4 +
 .../hive/ql/parse/AlterTableExecuteSpec.java       |   3 +-
 8 files changed, 243 insertions(+), 1 deletion(-)

diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/HiveIcebergStorageHandler.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/HiveIcebergStorageHandler.java
index 070bd9b70ae..c6982640d4a 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/HiveIcebergStorageHandler.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/HiveIcebergStorageHandler.java
@@ -1358,6 +1358,9 @@ public void 
executeOperation(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
             (AlterTableExecuteSpec.CherryPickSpec) 
executeSpec.getOperationParams();
         IcebergTableUtil.cherryPick(icebergTable, 
cherryPickSpec.getSnapshotId());
         break;
+      case REWRITE_MANIFESTS:
+        IcebergTableUtil.rewriteManifests(icebergTable);
+        break;
       case DELETE_METADATA:
         AlterTableExecuteSpec.DeleteMetadataSpec deleteMetadataSpec =
             (AlterTableExecuteSpec.DeleteMetadataSpec) 
executeSpec.getOperationParams();
diff --git 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergTableUtil.java
 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergTableUtil.java
index ad2ff8e4c2f..dd613f452be 100644
--- 
a/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergTableUtil.java
+++ 
b/iceberg/iceberg-handler/src/main/java/org/apache/iceberg/mr/hive/IcebergTableUtil.java
@@ -33,6 +33,7 @@
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -69,11 +70,14 @@
 import org.apache.hadoop.hive.ql.session.SessionStateUtil;
 import org.apache.hadoop.util.Sets;
 import org.apache.iceberg.ContentFile;
+import org.apache.iceberg.DataFile;
 import org.apache.iceberg.DeleteFiles;
 import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.FileScanTask;
 import org.apache.iceberg.ManageSnapshots;
 import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
 import org.apache.iceberg.MetadataTableType;
 import org.apache.iceberg.MetadataTableUtils;
 import org.apache.iceberg.PartitionData;
@@ -110,11 +114,13 @@
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
 import org.apache.iceberg.transforms.Transform;
+import org.apache.iceberg.types.Comparators;
 import org.apache.iceberg.types.Conversions;
 import org.apache.iceberg.types.Type;
 import org.apache.iceberg.types.Types;
 import org.apache.iceberg.util.ByteBuffers;
 import org.apache.iceberg.util.Pair;
+import org.apache.iceberg.util.PartitionUtil;
 import org.apache.iceberg.util.SnapshotUtil;
 import org.apache.iceberg.util.StructProjection;
 import org.slf4j.Logger;
@@ -823,10 +829,115 @@ public static ExecutorService newDeleteThreadPool(String 
completeName, int numTh
     });
   }
 
+  public static void rewriteManifests(Table table) {
+    // Skip empty tables that do not have a snapshot yet
+    if (table.currentSnapshot() == null) {
+      return;
+    }
+
+    // Unpartitioned tables can be safely clustered into a single bucket
+    if (!table.spec().isPartitioned()) {
+      table.rewriteManifests().clusterBy(file -> 0).commit();
+      return;
+    }
+
+    // Determine the target size for each new manifest file (defaults to 8MB)
+    long manifestTargetSizeBytes = 
TableProperties.MANIFEST_TARGET_SIZE_BYTES_DEFAULT;
+    if 
(table.properties().containsKey(TableProperties.MANIFEST_TARGET_SIZE_BYTES)) {
+      manifestTargetSizeBytes =
+          
Long.parseLong(table.properties().get(TableProperties.MANIFEST_TARGET_SIZE_BYTES));
+    }
+
+    List<ManifestFile> dataManifests = 
table.currentSnapshot().dataManifests(table.io());
+    if (dataManifests.isEmpty()) {
+      return;
+    }
+
+    // Calculate ideal number of manifest files based on current total 
metadata size.
+    // We hard-cap the maximum number of clusters at 200 to prevent the JVM 
from
+    // opening too many concurrent file writers and causing OOM or OS ulimit 
(Too Many Open Files).
+    long totalManifestsSize = 
dataManifests.stream().mapToLong(ManifestFile::length).sum();
+    int targetClusters =
+        (int)
+            Math.min(
+                (totalManifestsSize + manifestTargetSizeBytes - 1) / 
manifestTargetSizeBytes, 200);
+
+    if (targetClusters <= 1) {
+      table.rewriteManifests().clusterBy(file -> 0).commit();
+      return;
+    }
+
+    // To cluster files efficiently, we want to group them naturally. We 
extract the native
+    // Type of the first partition column (e.g. Timestamp, String) and use 
Iceberg's native
+    // Comparators to maintain a sorted TreeSet of all unique partition values.
+    Type.PrimitiveType firstPartitionFieldType =
+        
table.spec().partitionType().fields().getFirst().type().asPrimitiveType();
+    Set<Object> uniqueValues =
+        getUniquePartitionValues(table, dataManifests, 
firstPartitionFieldType);
+
+    if (uniqueValues.isEmpty()) {
+      table.rewriteManifests().clusterBy(file -> 0).commit();
+      return;
+    }
+
+    // Divide the naturally sorted unique values evenly into our calculated 
`targetClusters`
+    Object[] sortedValues = uniqueValues.toArray();
+    Map<Object, Integer> valueToBucket = Maps.newHashMap();
+    for (int i = 0; i < sortedValues.length; i++) {
+      // e.g. If we have 1000 sorted partition values and 200 clusters, this 
groups 5 values per
+      // bucket ID
+      valueToBucket.put(sortedValues[i], i * targetClusters / 
sortedValues.length);
+    }
+
+    // Rewrite manifests, telling Iceberg to group data files based on our 
pre-calculated bucket
+    // mapping
+    table
+        .rewriteManifests()
+        .clusterBy(
+            file -> {
+              StructLike partition =
+                  PartitionUtil.coercePartition(
+                      table.spec().partitionType(),
+                      table.specs().get(file.specId()),
+                      file.partition());
+              Object value = partition.get(0, Object.class);
+              return value != null ? valueToBucket.getOrDefault(value, 0) : 0;
+            })
+        .commit();
+  }
+
   public static boolean hasUndergonePartitionEvolution(Table table) {
     return table.specs().size() > 1;
   }
 
+  private static Set<Object> getUniquePartitionValues(
+      Table table, List<ManifestFile> dataManifests, Type.PrimitiveType 
firstPartitionFieldType) {
+    Set<Object> uniqueValues = new 
TreeSet<>(Comparators.forType(firstPartitionFieldType));
+
+    for (ManifestFile manifestFile : dataManifests) {
+      try (ManifestReader<DataFile> reader =
+          ManifestFiles.read(manifestFile, table.io(), table.specs())
+              .select(List.of(DataFile.PARTITION_NAME))) {
+        for (DataFile dataFile : reader) {
+          // Coerce partition struct in case of partition evolution
+          StructLike partition =
+              PartitionUtil.coercePartition(
+                  table.spec().partitionType(),
+                  table.specs().get(dataFile.specId()),
+                  dataFile.partition());
+          // Only extract and sort by the FIRST partition column for read 
optimization
+          Object value = partition.get(0, Object.class);
+          if (value != null) {
+            uniqueValues.add(value);
+          }
+        }
+      } catch (IOException e) {
+        throw new RuntimeException("Failed to read manifest file", e);
+      }
+    }
+    return uniqueValues;
+  }
+
   public static boolean hasUndergonePartitionEvolution(Snapshot snapshot, 
FileIO io) {
     if (snapshot == null) {
       return false;
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergSnapshotOperations.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergSnapshotOperations.java
index b483df82e43..23229a3c8b7 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergSnapshotOperations.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergSnapshotOperations.java
@@ -119,4 +119,123 @@ public void testReplaceBranchWithSnapshot() {
     result = shell.executeStatement("SELECT COUNT(*) FROM 
default.testReplaceBranchWithSnapshot.branch_branch1");
     assertEquals(6L, result.get(0)[0]);
   }
+
+  /**
+   * Tests executing REWRITE_MANIFESTS on an unpartitioned table to verify that
+   * multiple small manifests are successfully compacted into a single 
optimized manifest.
+   */
+  @Test
+  public void testRewriteManifests() {
+    TableIdentifier identifier = TableIdentifier.of("default", 
"testRewriteManifests");
+    shell.executeStatement(
+        String.format(
+            "CREATE EXTERNAL TABLE %s (id INT, data STRING) STORED BY iceberg 
%s %s",
+            identifier.name(),
+            testTables.locationForCreateTableSQL(identifier),
+            
testTables.propertiesForCreateTableSQL(ImmutableMap.of("commit.manifest.min-count-to-compact",
 "2"))));
+
+    // Create 5 manifests by executing 5 separate INSERT operations
+    for (int i = 1; i <= 5; i++) {
+      shell.executeStatement(
+          String.format("INSERT INTO TABLE %s VALUES(%d, 'val')", 
identifier.name(), i));
+    }
+
+    int manifestCountAfterRewrite = generateManifestsAndRewrite(identifier, 5);
+    assertEquals("Should have exactly 1 manifest after REWRITE_MANIFESTS", 1, 
manifestCountAfterRewrite);
+  }
+
+  /**
+   * Tests executing REWRITE_MANIFESTS on a partitioned table to verify that 
multiple manifests
+   * spread across different partitions are correctly clustered and compacted 
into fewer, optimized
+   * manifests.
+   */
+  @Test
+  public void testRewriteManifestsPartitioned() {
+    TableIdentifier identifier = TableIdentifier.of("default", 
"testRewriteManifestsPartitioned");
+    shell.executeStatement(
+        String.format(
+            "CREATE EXTERNAL TABLE %s (id INT, data STRING) PARTITIONED BY 
(part STRING) STORED BY iceberg %s %s",
+            identifier.name(),
+            testTables.locationForCreateTableSQL(identifier),
+            testTables.propertiesForCreateTableSQL(
+                ImmutableMap.of("commit.manifest.min-count-to-compact", 
"2"))));
+
+    // Create 5 manifests by executing 5 separate INSERT operations across 2 
partitions
+    for (int i = 1; i <= 5; i++) {
+      String partitionVal = (i % 2 == 0) ? "p2" : "p1";
+      shell.executeStatement(
+          String.format(
+              "INSERT INTO TABLE %s VALUES(%d, 'val', '%s')", 
identifier.name(), i, partitionVal));
+    }
+
+    int manifestCountAfterRewrite = generateManifestsAndRewrite(identifier, 5);
+    assertEquals("Should have exactly 1 manifest after REWRITE_MANIFESTS", 1, 
manifestCountAfterRewrite);
+  }
+
+  /**
+   * Tests that executing REWRITE_MANIFESTS on an empty table (with no 
existing snapshots) safely
+   * acts as a no-op without throwing any exceptions.
+   */
+  @Test
+  public void testRewriteManifestsEmptyTable() {
+    TableIdentifier identifier = TableIdentifier.of("default", 
"testRewriteManifestsEmptyTable");
+    shell.executeStatement(
+        String.format(
+            "CREATE EXTERNAL TABLE %s (id INT) STORED BY iceberg %s %s",
+            identifier.name(),
+            testTables.locationForCreateTableSQL(identifier),
+            testTables.propertiesForCreateTableSQL(ImmutableMap.of())));
+
+    // Execute on a table with no snapshot
+    shell.executeStatement(
+        String.format("ALTER TABLE %s EXECUTE REWRITE_MANIFESTS", 
identifier.name()));
+
+    org.apache.iceberg.Table icebergTable = testTables.loadTable(identifier);
+    Assert.assertNull(icebergTable.currentSnapshot());
+  }
+
+  /**
+   * Tests REWRITE_MANIFESTS with a very small target size to ensure that 
multiple output manifests
+   * are created, proving the bucketing and splitting logic works.
+   */
+  @Test
+  public void testRewriteManifestsMultipleOutputManifests() {
+    TableIdentifier identifier =
+        TableIdentifier.of("default", "testRewriteManifestsMultipleOutputs");
+    shell.executeStatement(
+        String.format(
+            "CREATE EXTERNAL TABLE %s (id INT, data STRING) PARTITIONED BY 
(part STRING) STORED BY iceberg %s %s",
+            identifier.name(),
+            testTables.locationForCreateTableSQL(identifier),
+            testTables.propertiesForCreateTableSQL(
+                ImmutableMap.of(
+                    "commit.manifest.min-count-to-compact", "100",
+                    "commit.manifest.target-size-bytes",
+                        "1")))); // Force multiple clusters and splits
+
+    for (int i = 1; i <= 15; i++) {
+      String partitionVal = "p" + (i % 5);
+      shell.executeStatement(
+          String.format(
+              "INSERT INTO TABLE %s VALUES(%d, 'val', '%s')", 
identifier.name(), i, partitionVal));
+    }
+
+    int manifestCountAfterRewrite = generateManifestsAndRewrite(identifier, 
15);
+    Assert.assertTrue("Should have multiple manifests due to 
bucketing/splitting", manifestCountAfterRewrite > 1);
+  }
+
+  private int generateManifestsAndRewrite(TableIdentifier identifier, int 
expectedBefore) {
+    org.apache.iceberg.Table icebergTable = testTables.loadTable(identifier);
+    icebergTable.refresh();
+
+    int manifestCountBefore = 
icebergTable.currentSnapshot().allManifests(icebergTable.io()).size();
+    assertEquals(
+        "Manifests keep accumulating for each insert", expectedBefore, 
manifestCountBefore);
+
+    shell.executeStatement(
+        String.format("ALTER TABLE %s EXECUTE REWRITE_MANIFESTS", 
identifier.name()));
+
+    icebergTable.refresh();
+    return 
icebergTable.currentSnapshot().allManifests(icebergTable.io()).size();
+  }
 }
diff --git 
a/parser/src/java/org/apache/hadoop/hive/ql/parse/AlterClauseParser.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/AlterClauseParser.g
index 81a3ea6774c..f34c3aec7a0 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/AlterClauseParser.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/AlterClauseParser.g
@@ -534,6 +534,8 @@ alterStatementSuffixExecute
     -> ^(TOK_ALTERTABLE_EXECUTE KW_ROLLBACK $rollbackParam)
     | KW_EXECUTE KW_EXPIRE_SNAPSHOTS (LPAREN (expireParam=expression) RPAREN)?
     -> ^(TOK_ALTERTABLE_EXECUTE KW_EXPIRE_SNAPSHOTS $expireParam?)
+    | KW_EXECUTE KW_REWRITE_MANIFESTS
+    -> ^(TOK_ALTERTABLE_EXECUTE KW_REWRITE_MANIFESTS)
     | KW_EXECUTE KW_SET_CURRENT_SNAPSHOT LPAREN (snapshotParam=expression) 
RPAREN
     -> ^(TOK_ALTERTABLE_EXECUTE KW_SET_CURRENT_SNAPSHOT $snapshotParam)
     | KW_EXECUTE KW_FAST_FORWARD sourceBranch=StringLiteral 
(targetBranch=StringLiteral)?
diff --git a/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
index cb3404587d8..a96812d4969 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
@@ -398,6 +398,7 @@ KW_SPEC: 'SPEC';
 KW_SYSTEM_TIME: 'SYSTEM_TIME';
 KW_SYSTEM_VERSION: 'SYSTEM_VERSION';
 KW_EXPIRE_SNAPSHOTS: 'EXPIRE_SNAPSHOTS';
+KW_REWRITE_MANIFESTS: 'REWRITE_MANIFESTS';
 KW_SET_CURRENT_SNAPSHOT: 'SET_CURRENT_SNAPSHOT';
 KW_BRANCH: 'BRANCH';
 KW_SNAPSHOTS: 'SNAPSHOTS';
diff --git 
a/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
index ac9053cf284..37fd6187d16 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
@@ -1027,6 +1027,7 @@ nonReserved
     | KW_SPEC
     | KW_SYSTEM_TIME | KW_SYSTEM_VERSION
     | KW_EXPIRE_SNAPSHOTS
+    | KW_REWRITE_MANIFESTS
     | KW_SET_CURRENT_SNAPSHOT
     | KW_BRANCH | KW_SNAPSHOTS | KW_RETAIN | KW_RETENTION
     | KW_TAG
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/execute/AlterTableExecuteAnalyzer.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/execute/AlterTableExecuteAnalyzer.java
index baba8353430..13bb7aa6f7f 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/execute/AlterTableExecuteAnalyzer.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/execute/AlterTableExecuteAnalyzer.java
@@ -103,6 +103,10 @@ protected void analyzeCommand(TableName tableName, 
Map<String, String> partition
       case HiveParser.KW_ORPHAN_FILES:
         desc = getDeleteOrphanFilesDesc(tableName, partitionSpec,  
command.getChildren());
         break;
+      case HiveParser.KW_REWRITE_MANIFESTS:
+        desc = new AlterTableExecuteDesc(tableName, partitionSpec,
+            new 
AlterTableExecuteSpec(AlterTableExecuteSpec.ExecuteOperationType.REWRITE_MANIFESTS,
 null));
+        break;
     }
 
     rootTasks.add(TaskFactory.get(new DDLWork(getInputs(), getOutputs(), 
desc)));
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/parse/AlterTableExecuteSpec.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/AlterTableExecuteSpec.java
index 54c8df3573c..17b0b1631d1 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/AlterTableExecuteSpec.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/AlterTableExecuteSpec.java
@@ -43,7 +43,8 @@ public enum ExecuteOperationType {
     FAST_FORWARD,
     CHERRY_PICK,
     DELETE_METADATA,
-    DELETE_ORPHAN_FILES;
+    DELETE_ORPHAN_FILES,
+    REWRITE_MANIFESTS;
   }
 
   private final ExecuteOperationType operationType;

Reply via email to