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 c021ce567a0 HIVE-29781: Iceberg: Basic stats fetch optimization - bulk 
partition stats read with query-scoped cache (#6662)
c021ce567a0 is described below

commit c021ce567a0dfcebba720286d762a12b0f21b63e
Author: Denys Kuzmenko <[email protected]>
AuthorDate: Tue Aug 4 19:41:10 2026 +0300

    HIVE-29781: Iceberg: Basic stats fetch optimization - bulk partition stats 
read with query-scoped cache (#6662)
---
 .../java/org/apache/hadoop/hive/ql/ErrorMsg.java   |   1 +
 .../iceberg/mr/hive/HiveIcebergStorageHandler.java | 278 +++++++++++-----
 .../apache/iceberg/mr/hive/IcebergTableUtil.java   | 129 ++------
 .../iceberg/mr/hive/TestHiveIcebergStatistics.java | 361 +++++++++++++++++++++
 ...iceberg_truncate_partition_with_evolution.q.out | 151 +++++----
 .../src/test/results/positive/row_count.q.out      |  22 +-
 .../truncate_partitioned_iceberg_table.q.out       |   2 +
 .../ql/ddl/table/info/desc/DescTableOperation.java |   4 +-
 .../hadoop/hive/ql/metadata/DummyPartition.java    |  10 +
 .../hive/ql/metadata/HiveStorageHandler.java       |  56 ++--
 .../hadoop/hive/ql/optimizer/StatsOptimizer.java   |  56 +++-
 .../hadoop/hive/ql/parse/BaseSemanticAnalyzer.java |  17 +
 .../hive/ql/parse/ColumnStatsSemanticAnalyzer.java |   5 +
 .../hadoop/hive/ql/parse/PrunedPartitionList.java  |   5 +-
 .../hadoop/hive/ql/parse/SemanticAnalyzer.java     |   4 +
 .../apache/hadoop/hive/ql/stats/BasicStats.java    |  82 ++---
 .../hadoop/hive/ql/stats/BasicStatsNoJobTask.java  |  23 +-
 .../hadoop/hive/ql/stats/BasicStatsTask.java       |   9 +-
 .../apache/hadoop/hive/ql/stats/StatsUtils.java    | 120 ++++---
 .../hadoop/hive/ql/stats/TestBasicStats.java       |  28 +-
 20 files changed, 972 insertions(+), 391 deletions(-)

diff --git a/common/src/java/org/apache/hadoop/hive/ql/ErrorMsg.java 
b/common/src/java/org/apache/hadoop/hive/ql/ErrorMsg.java
index 156d7c7ebe7..a900e3eb60b 100644
--- a/common/src/java/org/apache/hadoop/hive/ql/ErrorMsg.java
+++ b/common/src/java/org/apache/hadoop/hive/ql/ErrorMsg.java
@@ -313,6 +313,7 @@ public enum ErrorMsg {
     "Not all clauses are supported with mapjoin hint. Please remove mapjoin 
hint."),
 
   ANALYZE_TABLE_NOSCAN_NON_NATIVE(10228, "ANALYZE TABLE NOSCAN cannot be used 
for " + "a non-native table"),
+  ANALYZE_PARTITION_NON_NATIVE(10449, "ANALYZE TABLE with a PARTITION clause 
cannot be used for a non-native table"),
 
   PARTITION_VALUE_NOT_CONTINUOUS(10234, "Partition values specified are not 
continuous." +
             " A subpartition value is specified without specifying the parent 
partition's value"),
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 41261ac4228..070bd9b70ae 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
@@ -29,7 +29,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -39,6 +38,7 @@
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
+import java.util.function.BiConsumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -46,6 +46,7 @@
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileStatus;
 import org.apache.hadoop.fs.FileSystem;
@@ -119,7 +120,6 @@
 import 
org.apache.hadoop.hive.ql.security.authorization.HiveCustomStorageHandlerUtils;
 import org.apache.hadoop.hive.ql.session.SessionState;
 import org.apache.hadoop.hive.ql.session.SessionStateUtil;
-import org.apache.hadoop.hive.ql.stats.Partish;
 import org.apache.hadoop.hive.ql.util.NullOrdering;
 import org.apache.hadoop.hive.serde2.AbstractSerDe;
 import org.apache.hadoop.hive.serde2.Deserializer;
@@ -153,7 +153,6 @@
 import org.apache.iceberg.PartitionStatistics;
 import org.apache.iceberg.PartitionStatisticsFile;
 import org.apache.iceberg.PartitionStatsHandler;
-import org.apache.iceberg.Partitioning;
 import org.apache.iceberg.RowLevelOperationMode;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.SchemaParser;
@@ -199,6 +198,7 @@
 import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
 import org.apache.iceberg.relocated.com.google.common.collect.FluentIterable;
 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.Iterables;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.relocated.com.google.common.collect.Maps;
@@ -237,6 +237,7 @@ public class HiveIcebergStorageHandler extends 
DefaultStorageHandler implements
   public static final String TABLE_DEFAULT_LOCATION = "TABLE_DEFAULT_LOCATION";
 
   private static final String PARTITION = "partition";
+  private static final String PARTITION_STATS_PREFIX = "partitionStats.";
   public static final String STATS = "/stats/snap-";
 
   public static final String COPY_ON_WRITE = 
RowLevelOperationMode.COPY_ON_WRITE.modeName();
@@ -504,68 +505,108 @@ public void 
appendFiles(org.apache.hadoop.hive.metastore.api.Table table, URI fr
   }
 
   @Override
-  public Map<String, String> getBasicStatistics(Partish partish) {
-    return getBasicStatistics(partish, false);
+  public Map<String, String> 
getBasicStatistics(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
+    return getBasicStatistics(hmsTable, false);
   }
 
-  @SuppressWarnings("checkstyle:CyclomaticComplexity")
-  private Map<String, String> getBasicStatistics(Partish partish, boolean 
quickStats) {
-    Map<String, String> stats = Maps.newHashMap();
+  private Map<String, String> 
getBasicStatistics(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      boolean quickStats) {
+    Map<String, String> stats;
 
-    org.apache.hadoop.hive.ql.metadata.Table hmsTable = partish.getTable();
     // For write queries where rows got modified, don't fetch from cache as 
values could have changed.
     Table table = getTable(hmsTable);
     Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
 
     if (snapshot == null) {
-      stats.put(StatsSetupConst.NUM_FILES, "0");
-      stats.put(StatsSetupConst.ROW_COUNT, "0");
-      stats.put(StatsSetupConst.TOTAL_SIZE, "0");
+      stats = emptyStatsMap();
 
     } else if (!HiveMetaHook.ICEBERG.equals(getStatsSource()) && !quickStats) {
-      stats = partish.getPartParameters();
+      stats = hmsTable.getParameters();
 
     } else {
-      Map<String, String> summary = getPartishSummary(partish, table, 
snapshot);
-      if (summary != null) {
-        if (summary.containsKey(TOTAL_DATA_FILES_PROP)) {
-          stats.put(StatsSetupConst.NUM_FILES, 
summary.get(TOTAL_DATA_FILES_PROP));
-        }
-        if (summary.containsKey(TOTAL_RECORDS_PROP) && !quickStats) {
-          long totalRecords = Long.parseLong(summary.get(TOTAL_RECORDS_PROP));
-          if (summary.containsKey(TOTAL_EQ_DELETES_PROP) &&
-              summary.containsKey(TOTAL_POS_DELETES_PROP)) {
+      Map<String, String> summary = snapshot.summary();
+      stats = summary != null ? toStatsMap(summary, quickStats) : Map.of();
+    }
+    return stats;
+  }
 
-            long totalEqDeletes = 
Long.parseLong(summary.get(TOTAL_EQ_DELETES_PROP));
-            long totalPosDeletes = 
Long.parseLong(summary.get(TOTAL_POS_DELETES_PROP));
+  private static Map<String, String> emptyStatsMap() {
+    return Map.of(
+        StatsSetupConst.NUM_FILES, "0",
+        StatsSetupConst.ROW_COUNT, "0",
+        StatsSetupConst.TOTAL_SIZE, "0");
+  }
 
-            long actualRecords = totalRecords - (totalEqDeletes > 0 ? 0 : 
totalPosDeletes);
-            totalRecords = actualRecords > 0 ? actualRecords : totalRecords;
-            // actualRecords maybe -ve in edge cases
-          }
-          stats.put(StatsSetupConst.ROW_COUNT, String.valueOf(totalRecords));
-        }
-        if (summary.containsKey(TOTAL_FILE_SIZE_PROP)) {
-          stats.put(StatsSetupConst.TOTAL_SIZE, 
summary.get(TOTAL_FILE_SIZE_PROP));
-        }
-      }
+  /**
+   * Converts the Iceberg counters into the basic statistics Hive reports: 
numFiles, numRows and totalSize.
+   *
+   * @param quickStats when true the record count is left out
+   */
+  private static Map<String, String> toStatsMap(Map<String, String> summary, 
boolean quickStats) {
+    Map<String, String> stats = Maps.newHashMap();
+    if (summary.containsKey(TOTAL_DATA_FILES_PROP)) {
+      stats.put(StatsSetupConst.NUM_FILES, summary.get(TOTAL_DATA_FILES_PROP));
+    }
+    if (summary.containsKey(TOTAL_RECORDS_PROP) && !quickStats) {
+      stats.put(StatsSetupConst.ROW_COUNT, 
String.valueOf(estimateNumRows(summary)));
+    }
+    if (summary.containsKey(TOTAL_FILE_SIZE_PROP)) {
+      stats.put(StatsSetupConst.TOTAL_SIZE, summary.get(TOTAL_FILE_SIZE_PROP));
     }
     return stats;
   }
 
+  private static Map<String, String> toStatsMap(PartitionStatistics stats) {
+    Map<String, Long> counters = Map.of(
+        StatsSetupConst.NUM_FILES, (long) stats.dataFileCount(),
+        StatsSetupConst.ROW_COUNT, estimateNumRows(stats.dataRecordCount(),
+            stats.equalityDeleteRecordCount(), 
stats.positionDeleteRecordCount()),
+        StatsSetupConst.TOTAL_SIZE, stats.totalDataFileSizeInBytes());
+    return ImmutableMap.copyOf(
+        Maps.transformValues(counters, String::valueOf));
+  }
+
+  private static long estimateNumRows(Map<String, String> summary) {
+    long totalRecords = Long.parseLong(summary.get(TOTAL_RECORDS_PROP));
+    if (!summary.containsKey(TOTAL_EQ_DELETES_PROP) || 
!summary.containsKey(TOTAL_POS_DELETES_PROP)) {
+      return totalRecords;
+    }
+    return estimateNumRows(totalRecords,
+        Long.parseLong(summary.get(TOTAL_EQ_DELETES_PROP)),
+        Long.parseLong(summary.get(TOTAL_POS_DELETES_PROP)));
+  }
+
+  /**
+   * Estimates the row count by subtracting position deletes from the record 
count. Falls back to the raw
+   * record count when equality deletes are present or the deletes 
over-subtract, as their exact effect is
+   * unknown from the counts alone.
+   */
+  private static long estimateNumRows(long totalRecords, long eqDeletes, long 
posDeletes) {
+    long numRows = totalRecords - (eqDeletes > 0 ? 0 : posDeletes);
+    return numRows > 0 ? numRows : totalRecords;
+  }
+
+  private static boolean hasNoDeletes(Map<String, String> summary) {
+    if (summary == null || !summary.containsKey(TOTAL_EQ_DELETES_PROP) ||
+        !summary.containsKey(TOTAL_POS_DELETES_PROP)) {
+      return false;
+    }
+    return Long.parseLong(summary.get(TOTAL_EQ_DELETES_PROP)) +
+        Long.parseLong(summary.get(TOTAL_POS_DELETES_PROP)) == 0;
+  }
+
   @Override
-  public Map<String, String> computeBasicStatistics(Partish partish) {
+  public Map<String, String> 
computeBasicStatistics(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
     Map<String, String> stats;
     if (!getStatsSource().equals(HiveMetaHook.ICEBERG)) {
-      stats = partish.getPartParameters();
+      stats = hmsTable.getParameters();
 
       if (!StatsSetupConst.areBasicStatsUptoDate(stats)) {
         // populate quick-stats
-        stats = getBasicStatistics(partish, true);
+        stats = getBasicStatistics(hmsTable, true);
       }
       return stats;
     }
-    org.apache.hadoop.hive.ql.metadata.Table hmsTable = partish.getTable();
     // For write queries where rows got modified, don't fetch from cache as 
values could have changed.
     Table table = getTable(hmsTable);
     Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
@@ -596,37 +637,78 @@ public Map<String, String> computeBasicStatistics(Partish 
partish) {
             .commit();
       }
     }
-    return getBasicStatistics(partish);
+    return getBasicStatistics(hmsTable);
   }
 
-  private static Map<String, String> getPartishSummary(Partish partish, Table 
table, Snapshot snapshot) {
-    if (partish.getPartition() != null) {
-      Types.StructType partitionType = Partitioning.partitionType(table);
-      try (CloseableIterable<PartitionStatistics> records =
-                   
table.newPartitionStatisticsScan().useSnapshot(snapshot.snapshotId()).scan()) {
-        Iterator<PartitionStatistics> recordsIterator = records.iterator();
-        if (!recordsIterator.hasNext()) {
-          LOG.warn("Partition stats file not found for snapshot: {}", 
snapshot.snapshotId());
-          return null;
-        }
-        String partName = partish.getPartition().getName();
-        while (recordsIterator.hasNext()) {
-          PartitionStatistics stats = recordsIterator.next();
-          PartitionSpec spec = table.specs().get(stats.specId());
-          PartitionData data = 
IcebergTableUtil.toPartitionData(stats.partition(), partitionType,
-                  spec.partitionType());
-          if (spec.partitionToPath(data).equals(partName)) {
-            return IcebergTableUtil.toStatsMap(stats);
-          }
-        }
+  @Override
+  public Map<String, Map<String, String>> 
getAggrBasicStatsFor(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      List<String> partNames) {
+    if (!HiveMetaHook.ICEBERG.equals(getStatsSource())) {
+      return Map.of();
+    }
+    Table table = getTable(hmsTable);
+    Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
 
-        LOG.warn("Partition {} not found in partition stats, falling back to 
metadata scan", partName);
-        return IcebergTableUtil.getPartitionStats(table, 
partish.getPartition().getSpec(), snapshot);
-      } catch (IOException e) {
-        throw new UncheckedIOException(e);
+    Map<String, Map<String, String>> result = 
Maps.newHashMapWithExpectedSize(partNames.size());
+    if (snapshot == null) {
+      partNames.forEach(partName -> result.put(partName, emptyStatsMap()));
+      return result;
+    }
+    collectPartitionStatsFor(table, snapshot, partNames,
+        (partName, stats) -> result.put(partName, toStatsMap(stats)));
+    return result;
+  }
+
+  private PartitionStatistics 
getPartitionStatsFor(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      String partName) {
+    if (!HiveMetaHook.ICEBERG.equals(getStatsSource())) {
+      return null;
+    }
+    Table table = getTable(hmsTable);
+    Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
+    return snapshot != null ?
+        getOrCachePartitionStats(table, snapshot).get(partName) : null;
+  }
+
+  /**
+   * Passes each named partition's statistics to {@code statsConsumer}, served 
from the snapshot's partition
+   * statistics file. Partitions missing from the file are skipped and logged.
+   */
+  private void collectPartitionStatsFor(Table table, Snapshot snapshot, 
List<String> partNames,
+      BiConsumer<String, PartitionStatistics> statsConsumer) {
+    Map<String, PartitionStatistics> partitionStats = 
getOrCachePartitionStats(table, snapshot);
+    int missing = 0;
+
+    for (String partName : partNames) {
+      PartitionStatistics stats = partitionStats.get(partName);
+      if (stats != null) {
+        statsConsumer.accept(partName, stats);
+      } else {
+        missing++;
       }
     }
-    return snapshot.summary();
+    if (missing > 0) {
+      LOG.warn("{} of {} partitions not found in the partition stats file of 
snapshot {}",
+          missing, partNames.size(), snapshot.snapshotId());
+    }
+  }
+
+  /**
+   * Returns the snapshot's partition statistics keyed by partition name, 
empty if the snapshot has no
+   * partition statistics file. The result is cached for the rest of the query 
under a key that includes
+   * the snapshot id, so repeated lookups share a single read and a new 
snapshot never sees an older
+   * snapshot's entry.
+   */
+  @SuppressWarnings("unchecked")
+  private Map<String, PartitionStatistics> getOrCachePartitionStats(Table 
table, Snapshot snapshot) {
+    String cacheKey = PARTITION_STATS_PREFIX + table.name() + '.' + 
snapshot.snapshotId();
+    Optional<Object> cached = SessionStateUtil.getResource(conf, cacheKey);
+    if (cached.isPresent()) {
+      return (Map<String, PartitionStatistics>) cached.get();
+    }
+    Map<String, PartitionStatistics> partitionStats = 
IcebergTableUtil.readPartitionStats(table, snapshot);
+    SessionStateUtil.addResource(conf, cacheKey, partitionStats);
+    return partitionStats;
   }
 
   private Table getTable(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
@@ -816,28 +898,55 @@ public AggrStats 
getAggrColStatsFor(org.apache.hadoop.hive.ql.metadata.Table hms
   }
 
   @Override
-  public boolean canComputeQueryUsingStats(Partish partish) {
-    org.apache.hadoop.hive.ql.metadata.Table hmsTable = partish.getTable();
+  public Long getRowCount(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
     if (hmsTable.getMetaTable() != null) {
-      return false;
-    }
-    if (!getStatsSource().equals(HiveMetaHook.ICEBERG) && 
StatsSetupConst.areBasicStatsUptoDate(
-          partish.getPartParameters())) {
-      return true;
+      return null;
     }
+    return getStatsSource().equals(HiveMetaHook.ICEBERG) ?
+        snapshotRowCount(hmsTable) : metastoreRowCount(hmsTable);
+  }
+
+  /** Row count from the HMS table parameters */
+  private static Long 
metastoreRowCount(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
+    Map<String, String> parameters = hmsTable.getParameters();
+    return StatsSetupConst.areBasicStatsUptoDate(parameters) ?
+        NumberUtils.createLong(parameters.get(StatsSetupConst.ROW_COUNT)) : 
null;
+  }
+
+  /** Row count from the snapshot summary */
+  private Long snapshotRowCount(org.apache.hadoop.hive.ql.metadata.Table 
hmsTable) {
     Table table = getTable(hmsTable);
     Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
-    if (snapshot != null) {
-      Map<String, String> summary = getPartishSummary(partish, table, 
snapshot);
-      if (summary != null && summary.containsKey(TOTAL_EQ_DELETES_PROP) &&
-          summary.containsKey(TOTAL_POS_DELETES_PROP)) {
+    if (snapshot == null) {
+      return null;
+    }
+    Map<String, String> summary = snapshot.summary();
+    return hasNoDeletes(summary) ? 
NumberUtils.createLong(summary.get(TOTAL_RECORDS_PROP)) : null;
+  }
 
-        long totalEqDeletes = 
Long.parseLong(summary.get(TOTAL_EQ_DELETES_PROP));
-        long totalPosDeletes = 
Long.parseLong(summary.get(TOTAL_POS_DELETES_PROP));
-        return totalEqDeletes + totalPosDeletes == 0;
-      }
+  @Override
+  public Map<String, Long> 
getRowCount(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      List<String> partNames) {
+    if (!HiveMetaHook.ICEBERG.equals(getStatsSource())) {
+      return Map.of();
     }
-    return false;
+    Table table = getTable(hmsTable);
+    Snapshot snapshot = IcebergTableUtil.getTableSnapshot(table, hmsTable);
+    if (snapshot == null) {
+      return Map.of();
+    }
+    if (partNames.stream().anyMatch(DummyPartition::isVoid)) {
+      // rows that belong to no partition are never pruned, so their count may 
include rows the predicate
+      // does not select
+      return Map.of();
+    }
+    Map<String, Long> rowCounts = 
Maps.newHashMapWithExpectedSize(partNames.size());
+    collectPartitionStatsFor(table, snapshot, partNames, (partName, stats) -> {
+      if (stats.equalityDeleteRecordCount() == 0 && 
stats.positionDeleteRecordCount() == 0) {
+        rowCounts.put(partName, stats.dataRecordCount());
+      }
+    });
+    return rowCounts;
   }
 
   private String getStatsSource() {
@@ -2216,9 +2325,9 @@ public Partition 
getPartition(org.apache.hadoop.hive.ql.metadata.Table table,
 
     // Populate basic statistics
     if (partition != null) {
-      Map<String, String> stats = getBasicStatistics(Partish.buildFor(table, 
partition));
-      if (stats != null && !stats.isEmpty()) {
-        partition.getTPartition().setParameters(stats);
+      PartitionStatistics stats = getPartitionStatsFor(table, 
partition.getName());
+      if (stats != null) {
+        partition.getTPartition().setParameters(toStatsMap(stats));
       }
     }
 
@@ -2367,12 +2476,11 @@ public List<Partition> 
getPartitionsByExpr(org.apache.hadoop.hive.ql.metadata.Ta
 
     try (CloseableIterable<FileScanTask> tasks = scan.planFiles()) {
       FluentIterable.from(tasks)
-          .filter(task -> task.spec().isPartitioned())
           .filter(task -> specFilter.test(task.file().specId()))
           .forEach(task -> {
             PartitionSpec spec = task.spec();
             PartitionData partitionData = 
IcebergTableUtil.toPartitionData(task.partition(), spec.partitionType());
-            String partName = spec.partitionToPath(partitionData);
+            String partName = IcebergTableUtil.toPartitionName(spec, 
partitionData);
 
             Map<String, String> partSpecMap =
                 IcebergTableUtil.makeSpecFromName(partName, spec, 
partitionData, defaultPartitionName);
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 6e4f50ff3e2..ad2ff8e4c2f 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
@@ -20,9 +20,11 @@
 package org.apache.iceberg.mr.hive;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.time.ZoneId;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
@@ -84,7 +86,6 @@
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.Snapshot;
 import org.apache.iceberg.SnapshotRef;
-import org.apache.iceberg.SnapshotSummary;
 import org.apache.iceberg.StatisticsFile;
 import org.apache.iceberg.StructLike;
 import org.apache.iceberg.Table;
@@ -105,7 +106,6 @@
 import org.apache.iceberg.puffin.Puffin;
 import org.apache.iceberg.puffin.PuffinReader;
 import org.apache.iceberg.relocated.com.google.common.collect.FluentIterable;
-import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 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;
@@ -482,6 +482,15 @@ public static PartitionData toPartitionData(StructLike 
sourceKey, Types.StructTy
     return toPartitionData(projection, targetKeyType);
   }
 
+  /**
+   * Returns the partition name of the given tuple, or {@link 
DummyPartition#VOID} for the empty
+   * name an unpartitioned spec renders. Statistics and partition pruning join 
on this name, so both must
+   * render it the same way.
+   */
+  static String toPartitionName(PartitionSpec spec, PartitionData data) {
+    return StringUtils.defaultIfEmpty(spec.partitionToPath(data), 
DummyPartition.VOID);
+  }
+
   /**
    * Builds a filter expression for data table operations 
(deleteFromRowFilter, FindFiles).
    * Only supports identity transforms. Keys are partition field names.
@@ -527,20 +536,6 @@ private static Expression buildPartitionsFilter(Table 
table, Map<String, String>
         bySourceColumn::containsKey);
   }
 
-  /**
-   * Builds a filter expression for the PARTITIONS metadata table from 
partition field names
-   * with already-transformed values (e.g. {key_bucket_8: 5}).
-   */
-  private static Expression buildPartitionsFilterFromPath(Table table, 
Map<String, String> partitionSpec)
-      throws SemanticException {
-    Types.StructType partitionType = Partitioning.partitionType(table);
-
-    return buildExpression(partitionSpec,
-        (column, value) ->
-            buildFieldPredicate(partitionType, column, value, "partition."),
-        col -> partitionType.field(col) != null);
-  }
-
   static List<PartitionField> getPartitionFields(Table table, boolean 
latestSpecOnly) {
     if (latestSpecOnly) {
       return table.spec().fields();
@@ -723,7 +718,7 @@ private static List<String> getPartitionNames(Configuration 
conf,
             }
             PartitionData data = toPartitionData(
                 row.get(PART_IDX, StructProjection.class), partitionType, 
spec.partitionType());
-            return Maps.immutableEntry(spec.partitionToPath(data), 
spec.specId());
+            return Maps.immutableEntry(toPartitionName(spec, data), 
spec.specId());
           })
           .filter(Objects::nonNull)
           .toSortedList(specIdComparator).stream()
@@ -736,86 +731,33 @@ private static List<String> 
getPartitionNames(Configuration conf,
   }
 
   /**
-   * Returns aggregated partition statistics from the PARTITIONS metadata 
table as a summary map.
-   * @param table the iceberg table
-   * @param partitionSpec partition spec map for filtering, or null for all 
partitions
-   * @return summary map with record count, file count, file size, and delete 
counts
+   * Reads the given snapshot's partition statistics file in a single pass, 
keyed by partition name;
+   * empty when the file is missing.
    */
-  static Map<String, String> getPartitionStats(Table table, Map<String, 
String> partitionSpec,
-      Snapshot snapshot) throws IOException {
-    PartitionsTable partitionsTable = (PartitionsTable) 
MetadataTableUtils.createMetadataTableInstance(
-        table, MetadataTableType.PARTITIONS);
-
-    Expression filter;
-    TableScan scan = partitionsTable.newScan();
-    if (snapshot != null) {
-      scan = scan.useSnapshot(snapshot.snapshotId());
-    }
-    try {
-      filter = buildPartitionsFilterFromPath(table, partitionSpec);
-      scan = scan.filter(filter);
-    } catch (SemanticException e) {
-      throw new IOException("Failed to build partition filter for " + 
partitionSpec, e);
-    }
-
-    Evaluator evaluator = new Evaluator(partitionsTable.schema().asStruct(), 
filter);
-
-    try (CloseableIterable<FileScanTask> fileScanTasks = scan.planFiles()) {
-      return FluentIterable.from(fileScanTasks)
-          .transformAndConcat(task -> task.asDataTask().rows())
-          .filter(evaluator::eval)
-          .transform(BasicPartitionStats::from)
-          .stream().reduce(BasicPartitionStats::merge)
-          .orElse(BasicPartitionStats.EMPTY)
-          .toStatsMap();
-    }
-  }
-
-  static Map<String, String> toStatsMap(PartitionStatistics stats) {
-    return ImmutableMap.of(
-        SnapshotSummary.TOTAL_DATA_FILES_PROP, 
String.valueOf(stats.dataFileCount()),
-        SnapshotSummary.TOTAL_RECORDS_PROP, 
String.valueOf(stats.dataRecordCount()),
-        SnapshotSummary.TOTAL_EQ_DELETES_PROP, 
String.valueOf(stats.equalityDeleteRecordCount()),
-        SnapshotSummary.TOTAL_POS_DELETES_PROP, 
String.valueOf(stats.positionDeleteRecordCount()),
-        SnapshotSummary.TOTAL_FILE_SIZE_PROP, 
String.valueOf(stats.totalDataFileSizeInBytes())
-    );
-  }
-
-  /** The subset of partition stats counters Hive reports, summed across 
PARTITIONS metadata table rows. */
-  private record BasicPartitionStats(
-      long recordCount, long fileCount, long totalFileSizeInBytes,
-      long positionDeleteRecordCount, long equalityDeleteRecordCount) {
-
-    static final BasicPartitionStats EMPTY = new BasicPartitionStats(0,
-        0, 0, 0, 0);
-
-    static BasicPartitionStats from(StructLike row) {
-      return new BasicPartitionStats(
-          row.get(2, Long.class),      // record_count
-          row.get(3, Integer.class),   // file_count
-          row.get(4, Long.class),      // total_data_file_size_in_bytes
-          row.get(5, Long.class),      // position_delete_record_count
-          row.get(7, Long.class));     // equality_delete_record_count
-    }
-
-    BasicPartitionStats merge(BasicPartitionStats other) {
-      return new BasicPartitionStats(
-          recordCount + other.recordCount,
-          fileCount + other.fileCount,
-          totalFileSizeInBytes + other.totalFileSizeInBytes,
-          positionDeleteRecordCount + other.positionDeleteRecordCount,
-          equalityDeleteRecordCount + other.equalityDeleteRecordCount);
+  static Map<String, PartitionStatistics> readPartitionStats(Table table, 
Snapshot snapshot) {
+    PartitionStatisticsFile statsFile = getPartitionStatsFile(table, 
snapshot.snapshotId());
+    if (statsFile == null) {
+      LOG.warn("Partition stats file not found for snapshot: {}", 
snapshot.snapshotId());
+      return Map.of();
     }
+    Map<String, PartitionStatistics> result = Maps.newHashMap();
+    Types.StructType partitionType = Partitioning.partitionType(table);
 
-    Map<String, String> toStatsMap() {
-      return ImmutableMap.of(
-          SnapshotSummary.TOTAL_DATA_FILES_PROP, String.valueOf(fileCount),
-          SnapshotSummary.TOTAL_RECORDS_PROP, String.valueOf(recordCount),
-          SnapshotSummary.TOTAL_EQ_DELETES_PROP, 
String.valueOf(equalityDeleteRecordCount),
-          SnapshotSummary.TOTAL_POS_DELETES_PROP, 
String.valueOf(positionDeleteRecordCount),
-          SnapshotSummary.TOTAL_FILE_SIZE_PROP, 
String.valueOf(totalFileSizeInBytes)
-      );
+    try (CloseableIterable<PartitionStatistics> records =
+        
table.newPartitionStatisticsScan().useSnapshot(snapshot.snapshotId()).scan()) {
+      LOG.info("Using partition stats from: {}", statsFile.path());
+      for (PartitionStatistics partitionStats : records) {
+        PartitionSpec spec = table.specs().get(partitionStats.specId());
+        PartitionData data = toPartitionData(partitionStats.partition(), 
partitionType,
+            spec.partitionType());
+        // the scan copies the counters into a fresh object per row, so 
retaining it is safe;
+        // only the partition tuple may alias the reader's reused struct - do 
not use it after this loop
+        result.put(toPartitionName(spec, data), partitionStats);
+      }
+    } catch (IOException e) {
+      throw new UncheckedIOException(e);
     }
+    return Collections.unmodifiableMap(result);
   }
 
   public static PartitionSpec getPartitionSpec(Table icebergTable, String 
partitionPath)
@@ -849,6 +791,7 @@ public static <T> List<T> readColStats(Table table, Long 
snapshotId, Predicate<B
 
     String statsPath = IcebergTableUtil.getColStatsPath(table, snapshotId);
     if (statsPath == null) {
+      LOG.warn("Column stats file not found for snapshot: {}", snapshotId);
       return colStats;
     }
     try (PuffinReader reader = 
Puffin.read(table.io().newInputFile(statsPath)).build()) {
diff --git 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergStatistics.java
 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergStatistics.java
index fc5a2b981f9..df57578b705 100644
--- 
a/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergStatistics.java
+++ 
b/iceberg/iceberg-handler/src/test/java/org/apache/iceberg/mr/hive/TestHiveIcebergStatistics.java
@@ -21,13 +21,24 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hive.common.StatsSetupConst;
 import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.metastore.api.AggrStats;
+import org.apache.hadoop.hive.metastore.api.ColumnStatistics;
+import org.apache.hadoop.hive.metastore.api.ColumnStatisticsObj;
+import org.apache.hadoop.hive.ql.ErrorMsg;
+import org.apache.hadoop.hive.ql.metadata.DummyPartition;
+import org.apache.hadoop.hive.ql.metadata.Partition;
+import org.apache.hadoop.hive.ql.parse.SemanticException;
 import org.apache.iceberg.AssertHelpers;
+import org.apache.iceberg.FileFormat;
 import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.PartitionStatistics;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.catalog.TableIdentifier;
@@ -358,6 +369,356 @@ public void testIcebergColStatsPath() throws IOException {
         
HiveIcebergTestUtils.valueForRow(HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA,
 result));
   }
 
+  @Test
+  public void testGetAggrBasicStatsForPartitioned() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+
+    List<String> partNames = partitionNames(handler, hmsTable);
+    Assert.assertEquals(3, partNames.size());
+
+    Map<String, Map<String, String>> aggr = 
handler.getAggrBasicStatsFor(hmsTable, partNames);
+    Assert.assertEquals(3, aggr.size());
+
+    // each customer record lands in its own last_name partition
+    for (String partName : partNames) {
+      Map<String, String> basicStats = aggr.get(partName);
+      Assert.assertEquals("1", basicStats.get(StatsSetupConst.ROW_COUNT));
+      Assert.assertEquals("1", basicStats.get(StatsSetupConst.NUM_FILES));
+      
Assert.assertTrue(Long.parseLong(basicStats.get(StatsSetupConst.TOTAL_SIZE)) > 
0);
+    }
+    // no deletes: every partition's row count is answered exactly
+    Map<String, Long> rowCounts = handler.getRowCount(hmsTable, partNames);
+    Assert.assertEquals(partNames.size(), rowCounts.size());
+    rowCounts.values().forEach(rowCount -> 
Assert.assertEquals(Long.valueOf(1), rowCount));
+  }
+
+  @Test
+  public void testAnalyzePartitionSpecRejected() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+    String expected = ErrorMsg.ANALYZE_PARTITION_NON_NATIVE.getMsg();
+
+    // basic statistics are maintained incrementally for all partitions as a 
whole:
+    // a partition-scoped basic-stats ANALYZE cannot be honored and must be 
rejected
+    AssertHelpers.assertThrows(
+        "Should reject partition-scoped basic-stats ANALYZE for non-native 
partitioned tables",
+        IllegalArgumentException.class, expected,
+        () -> shell.executeStatement(
+            "ANALYZE TABLE " + identifier + " PARTITION (last_name='Brown') 
COMPUTE STATISTICS")
+    );
+    // same for column statistics: the rewrite would drop every other 
partition's column stats
+    AssertHelpers.assertThrows(
+        "Should reject partition-scoped column-stats ANALYZE for non-native 
partitioned tables",
+        IllegalArgumentException.class, expected,
+        () -> shell.executeStatement(
+            "ANALYZE TABLE " + identifier + " PARTITION (last_name='Brown') 
COMPUTE STATISTICS FOR COLUMNS")
+    );
+  }
+
+  @Test
+  public void testAnalyzeCatchesUpPartitionStats() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+
+    // a write that skips the stats auto-gathering leaves the new snapshot 
without a partition stats file
+    shell.setHiveSessionValue(HiveConf.ConfVars.HIVE_STATS_AUTOGATHER.varname, 
false);
+    shell.executeStatement(testTables.getInsertQuery(
+        HiveIcebergStorageHandlerTestUtils.OTHER_CUSTOMER_RECORDS_2, 
identifier, false));
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+    List<String> partNames = partitionNames(handler, hmsTable);
+
+    // the partition stats file is missing: every partition is reported 
missing (estimated by the planner)
+    // and exact query answering is refused
+    Assert.assertEquals(0, handler.getAggrBasicStatsFor(hmsTable, 
partNames).size());
+    Assert.assertTrue(handler.getRowCount(hmsTable, partNames).isEmpty());
+
+    // ANALYZE catches up: the incremental computation publishes the partition 
stats file
+    shell.executeStatement("ANALYZE TABLE " + identifier + " COMPUTE 
STATISTICS");
+
+    Map<String, Map<String, String>> refreshed = 
handler.getAggrBasicStatsFor(hmsTable, partNames);
+    Assert.assertEquals(partNames.size(), refreshed.size());
+    refreshed.values().forEach(basicStats ->
+        
Assert.assertTrue(Long.parseLong(basicStats.get(StatsSetupConst.ROW_COUNT)) > 
0));
+    Assert.assertEquals(partNames.size(), handler.getRowCount(hmsTable, 
partNames).size());
+  }
+
+  @Test
+  public void testGetAggrColStatsForPartitioned() throws Exception {
+    // bulk partition column statistics: a single puffin read aggregated 
across the requested
+    // partitions, mirroring testGetAggrBasicStatsForPartitioned
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+
+    List<String> partNames = partitionNames(handler, hmsTable);
+    Assert.assertEquals(3, partNames.size());
+
+    AggrStats aggrStats = handler.getAggrColStatsFor(hmsTable, 
ImmutableList.of("customer_id"), partNames);
+    Assert.assertEquals(3, aggrStats.getPartsFound());
+    Assert.assertEquals(1, aggrStats.getColStatsSize());
+    ColumnStatisticsObj statsObj = aggrStats.getColStats().get(0);
+    Assert.assertEquals("customer_id", statsObj.getColName());
+    // customer ids 0..2, one per last_name partition, merged across the three 
partitions
+    Assert.assertEquals(0, 
statsObj.getStatsData().getLongStats().getLowValue());
+    Assert.assertEquals(2, 
statsObj.getStatsData().getLongStats().getHighValue());
+  }
+
+  @Test
+  public void testCountStarPartitioned() {
+    // end-to-end count(*) correctness on a partitioned table, for both stats 
sources:
+    // iceberg source answers from the per-partition stats (exactness gate), 
metastore source must
+    // not fail (regression: NumberFormatException on the missing 
per-partition ROW_COUNT) and a
+    // partition-pruned count(*) must not be answered from the table-level row 
count
+    assumeParquetHiveCatalog();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+
+    List<Object[]> result = shell.executeStatement("SELECT count(*) FROM " + 
identifier);
+    Assert.assertEquals(3L, result.get(0)[0]);
+
+    result = shell.executeStatement("SELECT count(*) FROM " + identifier + " 
WHERE last_name = 'Brown'");
+    Assert.assertEquals(1L, result.get(0)[0]);
+
+    if (statsSource.equals("iceberg")) {
+      // the counts must be answered from statistics: the rewritten plans 
contain no table scan
+      String plan = shell.executeAndStringify("EXPLAIN SELECT count(*) FROM " 
+ identifier);
+      Assert.assertFalse(plan, plan.contains("TableScan"));
+      plan = shell.executeAndStringify("EXPLAIN SELECT count(*) FROM " + 
identifier + " WHERE last_name = 'Brown'");
+      Assert.assertFalse(plan, plan.contains("TableScan"));
+    }
+  }
+
+  @Test
+  public void testCountStarWithoutPartitionStatsFile() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier, false);
+
+    Table icebergTable = testTables.loadTable(identifier);
+    Assert.assertNull(IcebergTableUtil.getPartitionStatsFile(
+        icebergTable, icebergTable.currentSnapshot().snapshotId()));
+
+    // the per-partition path needs that file, so a pruned count is answered 
by a scan, while the
+    // unpruned one still comes from the table-level statistics
+    Assert.assertEquals(1L, shell.executeStatement(
+        "SELECT count(*) FROM " + identifier + " WHERE last_name = 
'Brown'").get(0)[0]);
+    Assert.assertEquals(3L, shell.executeStatement("SELECT count(*) FROM " + 
identifier).get(0)[0]);
+    String plan = shell.executeAndStringify("EXPLAIN SELECT count(*) FROM " + 
identifier);
+    Assert.assertFalse(plan, plan.contains("TableScan"));
+  }
+
+  @Test
+  public void testRowCountWithDeletes() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createPartitionedCustomers(identifier);
+    shell.executeStatement(testTables.getInsertQuery(
+        HiveIcebergStorageHandlerTestUtils.OTHER_CUSTOMER_RECORDS_2, 
identifier, false));
+    // position-delete one of the two Silver rows (the default delete mode is 
copy-on-write, which would
+    // rewrite the file and leave no delete file behind)
+    shell.executeStatement("ALTER TABLE " + identifier + " SET TBLPROPERTIES 
('write.delete.mode'='merge-on-read')");
+    shell.executeStatement("DELETE FROM " + identifier + " WHERE customer_id = 
2 AND first_name = 'Joanna'");
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+    List<String> partNames = partitionNames(handler, hmsTable);
+    Assert.assertEquals(11, partNames.size());
+
+    // the delete-covered partition's count is inexact and must be omitted; 
all others stay exact
+    Map<String, Long> rowCounts = handler.getRowCount(hmsTable, partNames);
+    Assert.assertEquals(10, rowCounts.size());
+    Assert.assertFalse(rowCounts.containsKey("last_name=Silver"));
+
+    // end-to-end: the rewrite is refused for the delete-covered scope and the 
counts stay correct
+    Assert.assertEquals(11L, shell.executeStatement("SELECT count(*) FROM " + 
identifier).get(0)[0]);
+    Assert.assertEquals(1L,
+        shell.executeStatement("SELECT count(*) FROM " + identifier + " WHERE 
last_name = 'Silver'").get(0)[0]);
+  }
+
+  @Test
+  public void testStatsAfterEvolutionFromUnpartitioned() throws Exception {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createEvolvedCustomers(identifier);
+
+    // ANALYZE computes both stats families on the evolved table
+    shell.executeStatement("ANALYZE TABLE " + identifier + " COMPUTE 
STATISTICS FOR COLUMNS");
+    checkColStat(identifier.name(), "customer_id", true);
+    Table icebergTable = testTables.loadTable(identifier);
+    validateBasicStats(icebergTable, "default", identifier.name());
+
+    // the partition stats file accounts for the legacy rows too, under the 
empty partition tuple
+    Map<String, PartitionStatistics> fileStats =
+        IcebergTableUtil.readPartitionStats(icebergTable, 
icebergTable.currentSnapshot());
+    Assert.assertEquals(3L, 
fileStats.get(DummyPartition.VOID).dataRecordCount().longValue());
+
+    // column stats blobs are written for the physical partitions only: values 
existing solely among the
+    // legacy unpartitioned rows (Green, Pink) get no blob
+    List<ColumnStatistics> colStats =
+        IcebergTableUtil.readColStats(icebergTable, 
icebergTable.currentSnapshot().snapshotId(), null);
+    Assert.assertEquals(
+        List.of("last_name=Barna", "last_name=Brown", "last_name=Rozsaszin", 
"last_name=Zold"),
+        colStats.stream().map(stats -> 
stats.getStatsDesc().getPartName()).sorted().toList());
+
+    // a blob describes the physical partition's files only: the legacy Brown 
row (customer_id 0) does not
+    // merge into the Brown partition's blob, which covers just the new-spec 
row (customer_id 3)
+    ColumnStatisticsObj brownCustomerId = colStats.stream()
+        .filter(stats -> 
"last_name=Brown".equals(stats.getStatsDesc().getPartName()))
+        .flatMap(stats -> stats.getStatsObj().stream())
+        .filter(obj -> "customer_id".equals(obj.getColName()))
+        .findFirst().orElseThrow();
+    Assert.assertEquals(3L, 
brownCustomerId.getStatsData().getLongStats().getLowValue());
+    Assert.assertEquals(3L, 
brownCustomerId.getStatsData().getLongStats().getHighValue());
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+    List<String> partNames = partitionNames(handler, hmsTable);
+    Assert.assertEquals(4, partNames.size());
+
+    // planner basic stats answer the requested partitions and carry the 
legacy rows as an extra entry
+    List<String> statNames = Lists.newArrayList(partNames);
+    statNames.add(DummyPartition.VOID);
+    Map<String, Map<String, String>> aggr = 
handler.getAggrBasicStatsFor(hmsTable, statNames);
+    Assert.assertEquals(statNames.size(), aggr.size());
+    partNames.forEach(name -> Assert.assertEquals("1", 
aggr.get(name).get(StatsSetupConst.ROW_COUNT)));
+    Assert.assertEquals("3",
+        aggr.get(DummyPartition.VOID).get(StatsSetupConst.ROW_COUNT));
+
+    // an unpruned scan plans with the table-level statistics; a pruned scan 
sums the matched partitions
+    // and the synthetic partition, which the pruner keeps whenever a legacy 
file survives file-level pruning
+    String plan = shell.executeAndStringify("EXPLAIN SELECT * FROM " + 
identifier);
+    Assert.assertTrue(plan, plan.contains("rows=7"));
+    // the legacy values span Brown..Pink, so the legacy file is pruned away 
for Barna
+    plan = shell.executeAndStringify("EXPLAIN SELECT * FROM " + identifier + " 
WHERE last_name = 'Barna'");
+    Assert.assertTrue(plan, plan.contains("rows=1"));
+    // Brown matches both the partition and the legacy file: 1 partition row + 
3 legacy rows are read
+    plan = shell.executeAndStringify("EXPLAIN SELECT * FROM " + identifier + " 
WHERE last_name = 'Brown'");
+    Assert.assertTrue(plan, plan.contains("rows=4"));
+    // Green has no partition of its own, only legacy rows: the synthetic 
partition alone answers
+    plan = shell.executeAndStringify("EXPLAIN SELECT * FROM " + identifier + " 
WHERE last_name = 'Green'");
+    Assert.assertTrue(plan, plan.contains("rows=3"));
+  }
+
+  @Test
+  public void testRowCountAfterEvolutionFromUnpartitioned() {
+    assumeParquetHiveCatalogIceberg();
+
+    TableIdentifier identifier = TableIdentifier.of("default", "customers");
+    createEvolvedCustomers(identifier);
+
+    org.apache.hadoop.hive.ql.metadata.Table hmsTable = hmsTable(identifier);
+    HiveIcebergStorageHandler handler = storageHandler();
+    List<String> partNames = partitionNames(handler, hmsTable);
+
+    // the synthetic partition cannot answer a predicate exactly, so a pruned 
list holding it is refused
+    List<String> withPseudo = Lists.newArrayList(partNames);
+    withPseudo.add(DummyPartition.VOID);
+    Assert.assertTrue(handler.getRowCount(hmsTable, withPseudo).isEmpty());
+    // without it the partition counts are exact
+    Assert.assertEquals(partNames.size(), handler.getRowCount(hmsTable, 
partNames).size());
+
+    // counts stay correct via a real scan, pruned and unpruned alike; Green 
exists only in the legacy
+    // rows so its pruned list is empty (partition statistics alone would have 
answered 0)
+    Assert.assertEquals(7L, shell.executeStatement("SELECT count(*) FROM " + 
identifier).get(0)[0]);
+    Assert.assertEquals(1L,
+        shell.executeStatement("SELECT count(*) FROM " + identifier + " WHERE 
last_name = 'Green'").get(0)[0]);
+    Assert.assertEquals(1L,
+        shell.executeStatement("SELECT count(*) FROM " + identifier + " WHERE 
last_name = 'Barna'").get(0)[0]);
+    // Brown is both a partition of its own and a value among the legacy rows: 
the synthetic partition
+    // keeps the filter in the plan, so only the two genuine Brown rows are 
returned
+    Assert.assertEquals(2L,
+        shell.executeStatement("SELECT count(*) FROM " + identifier + " WHERE 
last_name = 'Brown'").get(0)[0]);
+    List<Object[]> brown = shell.executeStatement(
+        "SELECT customer_id FROM " + identifier + " WHERE last_name = 'Brown' 
ORDER BY customer_id");
+    Assert.assertEquals(2, brown.size());
+    Assert.assertEquals(0L, brown.get(0)[0]);
+    Assert.assertEquals(3L, brown.get(1)[0]);
+  }
+
+  private void createEvolvedCustomers(TableIdentifier identifier) {
+    shell.setHiveSessionValue(HiveConf.ConfVars.HIVE_STATS_AUTOGATHER.varname, 
true);
+    testTables.createTable(shell, identifier.name(), 
HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA,
+        PartitionSpec.unpartitioned(), fileFormat, ImmutableList.of(), 
formatVersion);
+    shell.executeStatement(testTables.getInsertQuery(
+        HiveIcebergStorageHandlerTestUtils.CUSTOMER_RECORDS, identifier, 
false));
+    shell.executeStatement("ALTER TABLE " + identifier + " SET PARTITION SPEC 
(last_name)");
+    shell.executeStatement(testTables.getInsertQuery(
+        HiveIcebergStorageHandlerTestUtils.OTHER_CUSTOMER_RECORDS_1, 
identifier, false));
+    // a Brown row in the new spec: its partition's value is also present 
among the legacy rows
+    shell.executeStatement("INSERT INTO " + identifier + " VALUES (3, 'Alice', 
'Brown')");
+  }
+
+  /** The bulk-stats scenarios are independent of the file format and catalog; 
pin one combination. */
+  private void assumeParquetHiveCatalog() {
+    Assume.assumeTrue(fileFormat == FileFormat.PARQUET && testTableType == 
TestTableType.HIVE_CATALOG);
+  }
+
+  private void assumeParquetHiveCatalogIceberg() {
+    assumeParquetHiveCatalog();
+    Assume.assumeTrue(statsSource.equals("iceberg"));
+  }
+
+  private void createPartitionedCustomers(TableIdentifier identifier) {
+    createPartitionedCustomers(identifier, true);
+  }
+
+  /** Auto-gathering on write triggers computeBasicStatistics, which publishes 
the partition stats file. */
+  private void createPartitionedCustomers(TableIdentifier identifier, boolean 
autoGather) {
+    PartitionSpec spec = 
PartitionSpec.builderFor(HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA)
+        .identity("last_name").build();
+    shell.setHiveSessionValue(HiveConf.ConfVars.HIVE_STATS_AUTOGATHER.varname, 
autoGather);
+    testTables.createTable(shell, identifier.name(), 
HiveIcebergStorageHandlerTestUtils.CUSTOMER_SCHEMA, spec,
+        fileFormat, ImmutableList.of(), formatVersion);
+    shell.executeStatement(testTables.getInsertQuery(
+        HiveIcebergStorageHandlerTestUtils.CUSTOMER_RECORDS, identifier, 
false));
+  }
+
+  private org.apache.hadoop.hive.ql.metadata.Table hmsTable(TableIdentifier 
identifier) {
+    try {
+      return new org.apache.hadoop.hive.ql.metadata.Table(
+          shell.metastore().getTable("default", identifier.name()));
+    } catch (TException | InterruptedException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private HiveIcebergStorageHandler storageHandler() {
+    HiveIcebergStorageHandler handler = new HiveIcebergStorageHandler();
+    handler.setConf(shell.getHiveConf());
+    return handler;
+  }
+
+  private static List<String> partitionNames(HiveIcebergStorageHandler handler,
+      org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
+    try {
+      return handler.getPartitions(hmsTable, Collections.emptyMap(), 
true).stream()
+          .map(Partition::getName)
+          .toList();
+    } catch (SemanticException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
   private void checkColStat(String tableName, String colName, boolean 
accurate) {
     List<Object[]> rows = shell.executeStatement("DESCRIBE " + tableName + " " 
+ colName);
 
diff --git 
a/iceberg/iceberg-handler/src/test/results/positive/iceberg_truncate_partition_with_evolution.q.out
 
b/iceberg/iceberg-handler/src/test/results/positive/iceberg_truncate_partition_with_evolution.q.out
index 9218f556ab1..ea652fe65e9 100644
--- 
a/iceberg/iceberg-handler/src/test/results/positive/iceberg_truncate_partition_with_evolution.q.out
+++ 
b/iceberg/iceberg-handler/src/test/results/positive/iceberg_truncate_partition_with_evolution.q.out
@@ -85,19 +85,22 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_int
                   filterExpr: (a = 22) (type: boolean)
-                  Statistics: Num rows: 1 Data size: 88 Basic stats: COMPLETE 
Column stats: PARTIAL
-                  Select Operator
-                    expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 22 (type: int), b (type: string)
-                    outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                    Statistics: Num rows: 1 Data size: 480 Basic stats: 
COMPLETE Column stats: PARTIAL
-                    File Output Operator
-                      compressed: false
-                      Statistics: Num rows: 1 Data size: 480 Basic stats: 
COMPLETE Column stats: PARTIAL
-                      table:
-                          input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
-                          output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
-                          serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
-                          name: default.test_ice_int
+                  Statistics: Num rows: 3 Data size: 276 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Filter Operator
+                    predicate: (a = 22) (type: boolean)
+                    Statistics: Num rows: 3 Data size: 276 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Select Operator
+                      expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 22 (type: int), b (type: string)
+                      outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
+                      Statistics: Num rows: 3 Data size: 1440 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      File Output Operator
+                        compressed: false
+                        Statistics: Num rows: 3 Data size: 1440 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        table:
+                            input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+                            output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+                            serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+                            name: default.test_ice_int
             Execution mode: vectorized
 
   Stage: Stage-3
@@ -132,6 +135,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_int
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 11     ddd
+11     ddd
 33     rrfdfdf
 33     rrfdfdf
 PREHOOK: query: select `partition` from default.test_ice_int.partitions
@@ -177,6 +181,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_int
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 11     ddd
+11     ddd
 PREHOOK: query: select `partition` from default.test_ice_int.partitions
 PREHOOK: type: QUERY
 PREHOOK: Input: default@test_ice_int
@@ -275,19 +280,22 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_bigint
                   filterExpr: (a = 226784902765739L) (type: boolean)
-                  Statistics: Num rows: 1 Data size: 88 Basic stats: COMPLETE 
Column stats: PARTIAL
-                  Select Operator
-                    expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 226784902765739L (type: bigint), 
b (type: string)
-                    outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                    Statistics: Num rows: 1 Data size: 484 Basic stats: 
COMPLETE Column stats: PARTIAL
-                    File Output Operator
-                      compressed: false
-                      Statistics: Num rows: 1 Data size: 484 Basic stats: 
COMPLETE Column stats: PARTIAL
-                      table:
-                          input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
-                          output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
-                          serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
-                          name: default.test_ice_bigint
+                  Statistics: Num rows: 3 Data size: 288 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Filter Operator
+                    predicate: (a = 226784902765739L) (type: boolean)
+                    Statistics: Num rows: 3 Data size: 288 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Select Operator
+                      expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 226784902765739L (type: bigint), 
b (type: string)
+                      outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
+                      Statistics: Num rows: 3 Data size: 1452 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      File Output Operator
+                        compressed: false
+                        Statistics: Num rows: 3 Data size: 1452 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        table:
+                            input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+                            output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+                            serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+                            name: default.test_ice_bigint
             Execution mode: vectorized
 
   Stage: Stage-3
@@ -322,6 +330,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_bigint
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 115674892756   ddd
+115674892756   ddd
 3367849937755673       rrfdfdf
 3367849937755673       rrfdfdf
 PREHOOK: query: select `partition` from default.test_ice_bigint.partitions
@@ -367,6 +376,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_bigint
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 115674892756   ddd
+115674892756   ddd
 PREHOOK: query: select `partition` from default.test_ice_bigint.partitions
 PREHOOK: type: QUERY
 PREHOOK: Input: default@test_ice_bigint
@@ -491,19 +501,22 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_str
                   filterExpr: (b = 'ddd') (type: boolean)
-                  Statistics: Num rows: 1 Data size: 8 Basic stats: COMPLETE 
Column stats: PARTIAL
-                  Select Operator
-                    expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), a (type: bigint), 'ddd' (type: 
string)
-                    outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                    Statistics: Num rows: 1 Data size: 483 Basic stats: 
COMPLETE Column stats: PARTIAL
-                    File Output Operator
-                      compressed: false
-                      Statistics: Num rows: 1 Data size: 483 Basic stats: 
COMPLETE Column stats: PARTIAL
-                      table:
-                          input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
-                          output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
-                          serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
-                          name: default.test_ice_str
+                  Statistics: Num rows: 7 Data size: 665 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Filter Operator
+                    predicate: (b = 'ddd') (type: boolean)
+                    Statistics: Num rows: 7 Data size: 665 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Select Operator
+                      expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), a (type: bigint), 'ddd' (type: 
string)
+                      outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
+                      Statistics: Num rows: 7 Data size: 3381 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      File Output Operator
+                        compressed: false
+                        Statistics: Num rows: 7 Data size: 3381 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        table:
+                            input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+                            output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+                            serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+                            name: default.test_ice_str
             Execution mode: vectorized
 
   Stage: Stage-3
@@ -538,7 +551,12 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_str
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 115674892756   a"ab
+115674892756   a"ab
 115674892756   a'ab
+115674892756   a'ab
+2267849027657399057    eefe
+2267849027657399057    eefe
+2267849027657399057    eefe
 2267849027657399057    eefe
 2267849027657399057    eefe
 2267849027657399057    eefe
@@ -589,7 +607,12 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_str
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 115674892756   a"ab
+115674892756   a"ab
 115674892756   a'ab
+115674892756   a'ab
+2267849027657399057    eefe
+2267849027657399057    eefe
+2267849027657399057    eefe
 2267849027657399057    eefe
 2267849027657399057    eefe
 2267849027657399057    eefe
@@ -629,6 +652,9 @@ POSTHOOK: Output: hdfs://### HDFS PATH ###
 2267849027657399057    eefe
 2267849027657399057    eefe
 2267849027657399057    eefe
+2267849027657399057    eefe
+2267849027657399057    eefe
+2267849027657399057    eefe
 PREHOOK: query: select `partition` from default.test_ice_str.partitions
 PREHOOK: type: QUERY
 PREHOOK: Input: default@test_ice_str
@@ -716,17 +742,17 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_date
                   filterExpr: (b = DATE'2022-02-07') (type: boolean)
-                  Statistics: Num rows: 1 Data size: 56 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Statistics: Num rows: 2 Data size: 128 Basic stats: COMPLETE 
Column stats: NONE
                   Filter Operator
                     predicate: (b = DATE'2022-02-07') (type: boolean)
-                    Statistics: Num rows: 1 Data size: 56 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                     Select Operator
                       expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), a (type: bigint), 
DATE'2022-02-07' (type: date)
                       outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                      Statistics: Num rows: 1 Data size: 444 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                       File Output Operator
                         compressed: false
-                        Statistics: Num rows: 1 Data size: 444 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                         table:
                             input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
                             output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
@@ -904,19 +930,22 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_double
                   filterExpr: (a = 1.156748927566759E11D) (type: boolean)
-                  Statistics: Num rows: 1 Data size: 56 Basic stats: COMPLETE 
Column stats: PARTIAL
-                  Select Operator
-                    expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 1.156748927566759E11D (type: 
double), b (type: date)
-                    outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                    Statistics: Num rows: 1 Data size: 452 Basic stats: 
COMPLETE Column stats: PARTIAL
-                    File Output Operator
-                      compressed: false
-                      Statistics: Num rows: 1 Data size: 452 Basic stats: 
COMPLETE Column stats: PARTIAL
-                      table:
-                          input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
-                          output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
-                          serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
-                          name: default.test_ice_double
+                  Statistics: Num rows: 3 Data size: 192 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Filter Operator
+                    predicate: (a = 1.156748927566759E11D) (type: boolean)
+                    Statistics: Num rows: 3 Data size: 192 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Select Operator
+                      expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 1.156748927566759E11D (type: 
double), b (type: date)
+                      outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
+                      Statistics: Num rows: 3 Data size: 1356 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      File Output Operator
+                        compressed: false
+                        Statistics: Num rows: 3 Data size: 1356 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        table:
+                            input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
+                            output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
+                            serde: org.apache.iceberg.mr.hive.HiveIcebergSerDe
+                            name: default.test_ice_double
             Execution mode: vectorized
 
   Stage: Stage-3
@@ -951,6 +980,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_double
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 2.267849027657399E9    2022-08-07
+2.267849027657399E9    2022-08-07
 3.367849937755674E7    2022-08-09
 3.367849937755674E7    2022-08-09
 PREHOOK: query: select `partition` from default.test_ice_double.partitions
@@ -996,6 +1026,7 @@ POSTHOOK: type: QUERY
 POSTHOOK: Input: default@test_ice_double
 POSTHOOK: Output: hdfs://### HDFS PATH ###
 2.267849027657399E9    2022-08-07
+2.267849027657399E9    2022-08-07
 PREHOOK: query: select `partition` from default.test_ice_double.partitions
 PREHOOK: type: QUERY
 PREHOOK: Input: default@test_ice_double
@@ -1081,17 +1112,17 @@ STAGE PLANS:
                 TableScan
                   alias: test_ice_double_date
                   filterExpr: ((a = 1.156748927566759E11D) and (b = 
DATE'2022-02-07')) (type: boolean)
-                  Statistics: Num rows: 1 Data size: 56 Basic stats: COMPLETE 
Column stats: PARTIAL
+                  Statistics: Num rows: 2 Data size: 128 Basic stats: COMPLETE 
Column stats: NONE
                   Filter Operator
                     predicate: ((a = 1.156748927566759E11D) and (b = 
DATE'2022-02-07')) (type: boolean)
-                    Statistics: Num rows: 1 Data size: 56 Basic stats: 
COMPLETE Column stats: PARTIAL
+                    Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                     Select Operator
                       expressions: PARTITION__SPEC__ID (type: int), 
PARTITION__HASH (type: bigint), FILE__PATH (type: string), ROW__POSITION (type: 
bigint), PARTITION__PROJECTION (type: string), 1.156748927566759E11D (type: 
double), DATE'2022-02-07' (type: date)
                       outputColumnNames: _col0, _col1, _col2, _col3, _col4, 
_col5, _col6
-                      Statistics: Num rows: 1 Data size: 452 Basic stats: 
COMPLETE Column stats: PARTIAL
+                      Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                       File Output Operator
                         compressed: false
-                        Statistics: Num rows: 1 Data size: 452 Basic stats: 
COMPLETE Column stats: PARTIAL
+                        Statistics: Num rows: 2 Data size: 128 Basic stats: 
COMPLETE Column stats: NONE
                         table:
                             input format: 
org.apache.iceberg.mr.hive.HiveIcebergInputFormat
                             output format: 
org.apache.iceberg.mr.hive.HiveIcebergOutputFormat
diff --git a/iceberg/iceberg-handler/src/test/results/positive/row_count.q.out 
b/iceberg/iceberg-handler/src/test/results/positive/row_count.q.out
index 4303ea510c7..0ff1c86f210 100644
--- a/iceberg/iceberg-handler/src/test/results/positive/row_count.q.out
+++ b/iceberg/iceberg-handler/src/test/results/positive/row_count.q.out
@@ -249,8 +249,8 @@ Stage-0
           PARTITION_ONLY_SHUFFLE [RS_9]
             Group By Operator [GBY_8] (rows=1 width=8)
               Output:["_col0"],aggregations:["count()"]
-              Select Operator [SEL_7] (rows=14 width=5###)
-                TableScan [TS_0] (rows=14 width=5###)
+              Select Operator [SEL_7] (rows=11 width=1###)
+                TableScan [TS_0] (rows=11 width=1###)
                   default@llap_orders,llap_orders,Tbl:COMPLETE,Col:COMPLETE
 
 PREHOOK: query: explain insert into  llap_orders select * from llap_orders 
limit 100000
@@ -284,30 +284,30 @@ Stage-3
                 <-Reducer 2 [SIMPLE_EDGE] vectorized
                   SHUFFLE [RS_21]
                     PartitionCols:_col4, _col5
-                    Select Operator [SEL_20] (rows=14 width=420)
+                    Select Operator [SEL_20] (rows=11 width=420)
                       Output:["_col0","_col1","_col2","_col3","_col4","_col5"]
-                      Limit [LIM_19] (rows=14 width=420)
+                      Limit [LIM_19] (rows=11 width=420)
                         Number of rows:100000
                       <-Map 1 [CUSTOM_SIMPLE_EDGE] vectorized
                         PARTITION_ONLY_SHUFFLE [RS_18]
-                          Select Operator [SEL_17] (rows=14 width=420)
+                          Select Operator [SEL_17] (rows=11 width=420)
                             
Output:["_col0","_col1","_col2","_col3","_col4","_col5"]
-                            Limit [LIM_16] (rows=14 width=420)
+                            Limit [LIM_16] (rows=11 width=420)
                               Number of rows:100000
-                              TableScan [TS_0] (rows=14 width=420)
+                              TableScan [TS_0] (rows=11 width=420)
                                 
default@llap_orders,llap_orders,Tbl:COMPLETE,Col:NONE,Output:["orderid","quantity","itemid","tradets","p1","p2"]
               Reducer 4 vectorized
               File Output Operator [FS_29]
-                Select Operator [SEL_28] (rows=7 width=420)
+                Select Operator [SEL_28] (rows=5 width=420)
                   
Output:["_col0","_col1","_col2","_col3","_col4","_col5","_col6","_col7","_col8","_col9","_col10","_col11","_col12","_col13","_col14","_col15","_col16","_col17","_col18","_col19","_col20","_col21","_col22","_col23","_col24","_col25","_col26","_col27","_col28","_col29","_col30","_col31","_col32","_col33","_col34","_col35","_col36"]
-                  Group By Operator [GBY_27] (rows=7 width=420)
+                  Group By Operator [GBY_27] (rows=5 width=420)
                     
Output:["_col0","_col1","_col2","_col3","_col4","_col5","_col6","_col7","_col8","_col9","_col10","_col11","_col12","_col13","_col14","_col15","_col16","_col17","_col18","_col19","_col20","_col21","_col22","_col23","_col24","_col25","_col26"],aggregations:["min(VALUE._col0)","max(VALUE._col1)","count(VALUE._col2)","count(VALUE._col3)","compute_bit_vector_hll(VALUE._col4)","min(VALUE._col5)","max(VALUE._col6)","count(VALUE._col7)","compute_bit_vector_hll(VALUE._col8)","
 [...]
                   <-Reducer 2 [SIMPLE_EDGE] vectorized
                     SHUFFLE [RS_24]
                       PartitionCols:_col0, _col1
-                      Group By Operator [GBY_23] (rows=14 width=420)
+                      Group By Operator [GBY_23] (rows=11 width=420)
                         
Output:["_col0","_col1","_col2","_col3","_col4","_col5","_col6","_col7","_col8","_col9","_col10","_col11","_col12","_col13","_col14","_col15","_col16","_col17","_col18","_col19","_col20","_col21","_col22","_col23","_col24","_col25","_col26"],aggregations:["min(orderid)","max(orderid)","count(1)","count(orderid)","compute_bit_vector_hll(orderid)","min(quantity)","max(quantity)","count(quantity)","compute_bit_vector_hll(quantity)","min(itemid)","max(itemid)","count(
 [...]
-                        Select Operator [SEL_22] (rows=14 width=420)
+                        Select Operator [SEL_22] (rows=11 width=420)
                           
Output:["orderid","quantity","itemid","tradets","p1","p2"]
                            Please refer to the previous Select Operator 
[SEL_20]
 
diff --git 
a/iceberg/iceberg-handler/src/test/results/positive/truncate_partitioned_iceberg_table.q.out
 
b/iceberg/iceberg-handler/src/test/results/positive/truncate_partitioned_iceberg_table.q.out
index 36456ba85fc..526707f5ada 100644
--- 
a/iceberg/iceberg-handler/src/test/results/positive/truncate_partitioned_iceberg_table.q.out
+++ 
b/iceberg/iceberg-handler/src/test/results/positive/truncate_partitioned_iceberg_table.q.out
@@ -109,9 +109,11 @@ Table Parameters:
 #### A masked pattern was here ####
        metadata_location       hdfs://### HDFS PATH ###
        numFiles                4                   
+       numPartitions           4                   
        numRows                 9                   
        parquet.compression     zstd                
        previous_metadata_location      hdfs://### HDFS PATH ###
+       rawDataSize             0                   
        schema.name-mapping.default     [ {                 
                                          \"field-id\" : 1, 
                                          \"names\" : [ \"a\" ]
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/info/desc/DescTableOperation.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/info/desc/DescTableOperation.java
index 8d45892e17f..c9d89a2f427 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/info/desc/DescTableOperation.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/ddl/table/info/desc/DescTableOperation.java
@@ -54,7 +54,6 @@
 import org.apache.hadoop.hive.ql.metadata.TableConstraintsInfo;
 import org.apache.hadoop.hive.ql.plan.ColStatistics;
 import org.apache.hadoop.hive.ql.session.SessionState;
-import org.apache.hadoop.hive.ql.stats.Partish;
 import org.apache.hadoop.hive.ql.stats.StatsUtils;
 import org.apache.hadoop.hive.serde.serdeConstants;
 import org.apache.hadoop.hive.serde2.Deserializer;
@@ -152,8 +151,7 @@ private void getColumnsNoColumnPath(Table table, Partition 
partition, List<Field
         if (table.isNonNative()) {
           HiveStorageHandler sh = table.getStorageHandler();
           
-          sh.getBasicStatistics(Partish.buildFor(table)).forEach((k, v) -> 
-              valueMap.put(k, Longs.tryParse(v)));
+          sh.getBasicStatistics(table).forEach((k, v) -> valueMap.put(k, 
Longs.tryParse(v)));
           numParts = sh.getPartitionNames(table).size();
           
         } else {
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/metadata/DummyPartition.java 
b/ql/src/java/org/apache/hadoop/hive/ql/metadata/DummyPartition.java
index 9f871d05feb..5209879e66a 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/metadata/DummyPartition.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/metadata/DummyPartition.java
@@ -36,6 +36,16 @@
  */
 public class DummyPartition extends Partition {
 
+  /**
+   * Name of the synthetic partition holding the rows that belong to no 
partition, e.g. data written
+   * before the table was partitioned. Its partition spec is empty, so no 
predicate can exclude it.
+   */
+  public static final String VOID = "__NO_PARTITION__";
+
+  public static boolean isVoid(String partName) {
+    return VOID.equals(partName);
+  }
+
   private String name;
   private LinkedHashMap<String, String> partSpec;
   private List<String> values;
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/metadata/HiveStorageHandler.java 
b/ql/src/java/org/apache/hadoop/hive/ql/metadata/HiveStorageHandler.java
index c50f0b33b05..ae0528bc207 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/metadata/HiveStorageHandler.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/metadata/HiveStorageHandler.java
@@ -70,7 +70,6 @@
 import org.apache.hadoop.hive.ql.plan.TableDesc;
 import 
org.apache.hadoop.hive.ql.security.authorization.HiveAuthorizationProvider;
 import 
org.apache.hadoop.hive.ql.security.authorization.HiveCustomStorageHandlerUtils;
-import org.apache.hadoop.hive.ql.stats.Partish;
 import org.apache.hadoop.hive.serde2.AbstractSerDe;
 import org.apache.hadoop.mapred.InputFormat;
 import org.apache.hadoop.mapred.JobConf;
@@ -248,19 +247,19 @@ default Map<String, String> 
getOperatorDescProperties(OperatorDesc operatorDesc,
   /**
    * Returns basic statistics (numRows, numFiles, totalSize) calculated by the 
underlying storage handler
    * implementation.
-   * @param partish table/partition wrapper object
+   * @param hmsTable table object
    * @return map of basic statistics
    */
-  default Map<String, String> getBasicStatistics(Partish partish) {
+  default Map<String, String> 
getBasicStatistics(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
     return null;
   }
 
   /**
-   * Compute basic statistics (numRows, numFiles, totalSize) for the given 
table/partition.
-   * @param partish table/partition wrapper object
+   * Compute basic statistics (numRows, numFiles, totalSize) for the given 
table.
+   * @param hmsTable table object
    * @return map of basic statistics
    */
-  default Map<String, String> computeBasicStatistics(Partish partish) {
+  default Map<String, String> 
computeBasicStatistics(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
     return null;
   }
 
@@ -309,12 +308,24 @@ default List<ColumnStatisticsObj> 
getColStatistics(org.apache.hadoop.hive.ql.met
    * @param colNames list of column names
    * @param partNames list of partition names
    * @return AggrStats object
-  */ 
-  default AggrStats 
getAggrColStatsFor(org.apache.hadoop.hive.ql.metadata.Table table, List<String> 
colNames, 
+  */
+  default AggrStats 
getAggrColStatsFor(org.apache.hadoop.hive.ql.metadata.Table table, List<String> 
colNames,
         List<String> partNames) throws MetaException {
     return null;
   }
 
+  /**
+   * Fetches basic statistics (numRows, numFiles, totalSize) for the supplied 
partitions in a single batch.
+   * @param hmsTable table object
+   * @param partNames list of partition names
+   * @return map of partition name to basic statistics, omitting partitions 
that have none;
+   *         empty if the handler does not provide them
+   */
+  default Map<String, Map<String, String>> 
getAggrBasicStatsFor(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      List<String> partNames) {
+    return Map.of();
+  }
+
   /**
    * Set column stats for non-native tables
    * @param table table object
@@ -344,26 +355,27 @@ default boolean 
canSetColStatistics(org.apache.hadoop.hive.ql.metadata.Table tab
   }
 
   /**
-   * Check if the storage handler can answer a few queries like count(1) 
purely using statistics.
-   * @param partish table/partition wrapper object
-   * @return true if the storage handler can answer query using statistics
+   * Returns the row count of the table, letting queries like count(1) be 
answered from statistics.
+   * @param hmsTable table object
+   * @return the row count, or null if it cannot be determined exactly
    */
-  default boolean canComputeQueryUsingStats(Partish partish) {
-    return false;
+  default Long getRowCount(org.apache.hadoop.hive.ql.metadata.Table hmsTable) {
+    return null;
   }
 
   /**
-   * Check if the storage handler can answer a few queries like count(1) 
purely using statistics.
-   * @param table table wrapper object
-   *
-   * @deprecated since 4.0.1, will be removed in 5.0.0,
-   * use {@link #canComputeQueryUsingStats(Partish)} instead.
+   * Returns the row count of the given partitions, omitting the ones whose 
count cannot be determined
+   * exactly, e.g. because they are covered by delete files.
+   * @param hmsTable table object
+   * @param partNames list of partition names
+   * @return map of partition name to row count, empty if the handler cannot 
determine partition
+   *         row counts at all, e.g. when some rows do not belong to any 
partition
    */
-  @Deprecated
-  default boolean 
canComputeQueryUsingStats(org.apache.hadoop.hive.ql.metadata.Table table) {
-    return canComputeQueryUsingStats(Partish.buildFor(table));
+  default Map<String, Long> 
getRowCount(org.apache.hadoop.hive.ql.metadata.Table hmsTable,
+      List<String> partNames) {
+    return Map.of();
   }
-  
+
   /**
    *
    * Gets the storage format descriptor to be used for temp table for LOAD 
data.
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/StatsOptimizer.java 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/StatsOptimizer.java
index c3eacc59bd2..35cb5fd774f 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/StatsOptimizer.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/StatsOptimizer.java
@@ -51,9 +51,11 @@
 import org.apache.hadoop.hive.ql.lockmgr.LockException;
 import org.apache.hadoop.hive.ql.metadata.Hive;
 import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.ql.metadata.HiveStorageHandler;
 import org.apache.hadoop.hive.ql.metadata.Partition;
 import org.apache.hadoop.hive.ql.metadata.Table;
 import org.apache.hadoop.hive.ql.parse.ParseContext;
+import org.apache.hadoop.hive.ql.parse.PrunedPartitionList;
 import org.apache.hadoop.hive.ql.parse.SemanticException;
 import org.apache.hadoop.hive.ql.plan.AggregationDesc;
 import org.apache.hadoop.hive.ql.plan.ExprNodeColumnDesc;
@@ -88,7 +90,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.Stack;
-import java.util.stream.Collectors;
 
 
 /** There is a set of queries which can be answered entirely from statistics 
stored in metastore.
@@ -933,29 +934,60 @@ private Collection<List<ColumnStatisticsObj>> 
verifyAndGetPartColumnStats(
     }
 
     private Long getRowCnt(TableScanOperator tsOp, Table tbl) throws 
HiveException {
-      long rowCnt = 0L;
+      if (tbl.isNonNative()) {
+        return getRowCntFromStorageHandler(tsOp, tbl);
+      }
       final List<Partish> partishList;
-      if (tbl.isPartitioned() && 
StatsUtils.checkCanProvidePartitionStats(tbl)) {
+      if (tbl.isPartitioned()) {
         partishList = pctx.getPrunedPartitions(tsOp.getConf().getAlias(), 
tsOp).getPartitions().stream()
           .map(Partish::buildFor)
-          .collect(Collectors.toList());
+          .toList();
       } else {
         partishList = Lists.newArrayList(Partish.buildFor(tbl));
       }
+      long rowCnt = 0L;
       for (Partish partish : partishList) {
         Map<String, String> basicStats = partish.getPartParameters();
-        if (tbl.isNonNative()) {
-          if (!tbl.getStorageHandler().canComputeQueryUsingStats(partish)) {
-            return null;
-          }
-          basicStats = tbl.getStorageHandler().getBasicStatistics(partish);
-        } else if 
(!StatsUtils.areBasicStatsUptoDateForQueryAnswering(partish.getTable(), 
partish.getPartParameters())) {
+        if 
(!StatsUtils.areBasicStatsUptoDateForQueryAnswering(partish.getTable(), 
basicStats)) {
           return null;
         }
-        long partRowCnt = 
Long.parseLong(basicStats.get(StatsSetupConst.ROW_COUNT));
-        rowCnt += partRowCnt;
+        rowCnt += Long.parseLong(basicStats.get(StatsSetupConst.ROW_COUNT));
       }
       return rowCnt;
     }
+
+    private Long getRowCntFromStorageHandler(TableScanOperator tsOp, Table 
tbl) throws HiveException {
+      if (tbl.getMetaTable() != null) {
+        // metadata table scans cannot be answered from the data table's 
statistics
+        return null;
+      }
+      HiveStorageHandler storageHandler = tbl.getStorageHandler();
+      if (tbl.isPartitioned()) {
+        PrunedPartitionList prunedList = 
pctx.getPrunedPartitions(tsOp.getConf().getAlias(), tsOp);
+        if (!prunedList.getReferredPartCols().isEmpty()) {
+          // a partition predicate may have pruned the list to a subset, so 
only the
+          // per-partition statistics can answer exactly
+          if (!StatsUtils.checkCanProvidePartitionStats(tbl)) {
+            return null;
+          }
+          List<String> partNames = prunedList.getPartitions().stream()
+              .map(Partition::getName)
+              .toList();
+          if (partNames.isEmpty()) {
+            // the predicate matched nothing the scan would read
+            return 0L;
+          }
+          Map<String, Long> rowCounts = storageHandler.getRowCount(tbl, 
partNames);
+          if (rowCounts.size() != partNames.size()) {
+            // the rewrite substitutes a constant, so it needs an exact count 
for every partition
+            return null;
+          }
+          return rowCounts.values().stream().mapToLong(Long::longValue).sum();
+        }
+      }
+      // unpruned or unpartitioned: the table-level statistics are exactly the 
aggregate of every
+      // partition - answered without reading the partition stats file
+      return storageHandler.getRowCount(tbl);
+    }
   }
 }
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java
index 434e131b4d0..7d83425aff8 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/BaseSemanticAnalyzer.java
@@ -1726,6 +1726,23 @@ private static boolean getPartExprNodeDesc(ASTNode 
astNode, HiveConf conf,
     return result;
   }
 
+  /**
+   * Rejects a query that has a partition clause but targets a table that is 
never partitioned on the HMS-level.
+   * Even though the table is not partitioned from the HMS's point of view, it 
might have some other notion of
+   * partitioning under the hood (e.g. Iceberg tables). In these cases, we 
might decide to proactively throw a more
+   * descriptive, unified error message instead of failing on some other 
semantic analysis validation step, which
+   * could provide a more counter-intuitive exception message.
+   *
+   * @param tbl The table object, should not be null.
+   * @param partitionClausePresent Whether a partition clause is present in 
the query (e.g. PARTITION(p='x'))
+   */
+  protected static void validateUnsupportedPartitionClause(Table tbl, boolean 
partitionClausePresent)
+      throws SemanticException {
+    if (partitionClausePresent && tbl.hasNonNativePartitionSupport()) {
+      throw new 
SemanticException(ErrorMsg.ANALYZE_PARTITION_NON_NATIVE.getMsg());
+    }
+  }
+
   /**
    * Get the partition specs from the tree.
    *
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/parse/ColumnStatsSemanticAnalyzer.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/ColumnStatsSemanticAnalyzer.java
index 88b4a4f0e9f..e952632d73b 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/parse/ColumnStatsSemanticAnalyzer.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/ColumnStatsSemanticAnalyzer.java
@@ -629,6 +629,11 @@ public void analyze(ASTNode ast, Context origCtx) throws 
SemanticException {
       originalTree = ast;
       boolean isPartitionStats = 
AnalyzeCommandUtils.isPartitionLevelStats(ast) 
           || StatsUtils.isPartitionStats(tbl, conf);
+      // a partition-scoped column statistics ANALYZE cannot be honored for 
non-native tables: the stats
+      // of all partitions are rewritten as a whole, so it would drop every 
other partition's statistics
+      // (table-level ANALYZE of a partitioned table still computes all 
partitions - only the explicit
+      // partition spec is rejected; the auto-gather path merges instead and 
stays unaffected)
+      validateUnsupportedPartitionClause(tbl, 
AnalyzeCommandUtils.isPartitionLevelStats(ast));
       
       Map<Integer, List<TransformSpec>> partTransformSpecs = 
Collections.singletonMap(-1, null);
       Map<String, String> partSpec = (isPartitionStats) ?
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/parse/PrunedPartitionList.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/PrunedPartitionList.java
index 3329d4ae749..0a4caa08c38 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/PrunedPartitionList.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/PrunedPartitionList.java
@@ -40,7 +40,10 @@ public class PrunedPartitionList {
   /** Partitions that either satisfy the partition criteria, or may satisfy 
it. */
   private final Set<Partition> partitions;
 
-  /** partition columns referred by pruner expr */
+  /**
+   * Partition columns referred by the pruner expr; empty when no partition 
predicate narrowed the list,
+   * or when the predicate was constant false - the list is empty in that case 
as well.
+   */
   private final List<String> referred;
 
   /** Whether there are partitions in the list that may or may not satisfy the 
criteria. */
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
index b728bd85402..7042cff5387 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
@@ -2472,6 +2472,10 @@ private void getMetaData(QB qb, ReadEntity parentInput)
       if (qb.getParseInfo().isAnalyzeCommand()) {
         // allow partial partition specification for nonscan since noscan is 
fast.
         TableSpec ts = new TableSpec(db, conf, (ASTNode) ast.getChild(0), 
true, this.noscan);
+        // basic statistics of non-native partitioned tables are maintained 
incrementally for all
+        // partitions as a whole, so a partition-scoped ANALYZE cannot be 
honored
+        validateUnsupportedPartitionClause(tab, ts.specType != 
SpecType.TABLE_ONLY);
+
         if (ts.specType == SpecType.DYNAMIC_PARTITION) { // dynamic partitions
           try {
             ts.partitions = db.getPartitionsByNames(ts.tableHandle, 
ts.partSpec);
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStats.java 
b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStats.java
index 507d5607aa7..780089310d1 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStats.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStats.java
@@ -25,7 +25,6 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -65,7 +64,11 @@ public void addEnhancer(IStatsEnhancer enhancer) {
     }
 
     public BasicStats build(Partish p) {
-      BasicStats ret = new BasicStats(p);
+      return build(p, null);
+    }
+
+    public BasicStats build(Partish p, Map<String, String> providedStats) {
+      BasicStats ret = new BasicStats(p, providedStats);
       for (IStatsEnhancer enhancer : enhancers) {
         ret.apply(enhancer);
       }
@@ -73,39 +76,27 @@ public BasicStats build(Partish p) {
     }
 
     public List<BasicStats> buildAll(HiveConf conf, Collection<Partish> parts) 
{
-      LOG.info("Number of partishes : " + parts.size());
+      LOG.info("Number of partishes : {}", parts.size());
 
-      final List<BasicStats> ret = new ArrayList<>(parts.size());
       if (parts.size() <= 1) {
-        for (Partish partish : parts) {
-          ret.add(build(partish));
-        }
-        return ret;
+        return parts.stream().map(this::build).toList();
       }
-
-      List<Future<BasicStats>> futures = new ArrayList<>();
-
       int threads = 
conf.getIntVar(ConfVars.METASTORE_FS_HANDLER_THREADS_COUNT);
-
-      final ExecutorService pool;
-      if (threads <= 1) {
-        pool = MoreExecutors.newDirectExecutorService();
-      } else {
-        pool = Executors.newFixedThreadPool(threads, new 
ThreadFactoryBuilder().setDaemon(true).setNameFormat("Get-Partitions-Size-%d").build());
-      }
-
-      for (final Partish part : parts) {
-        futures.add(pool.submit(new Callable<BasicStats>() {
-          @Override
-          public BasicStats call() throws Exception {
-            return build(part);
-          }
-        }));
-      }
-
+      ExecutorService pool = threads <= 1 ? 
MoreExecutors.newDirectExecutorService() :
+          Executors.newFixedThreadPool(threads,
+              new ThreadFactoryBuilder()
+                  .setDaemon(true)
+                  .setNameFormat("Get-Partitions-Size-%d")
+                  .build());
+
+      List<Future<BasicStats>> futures = parts.stream()
+          .map(part -> pool.submit(() -> build(part)))
+          .toList();
+
+      List<BasicStats> ret = new ArrayList<>(parts.size());
       try {
-        for (int i = 0; i < futures.size(); i++) {
-          ret.add(i, futures.get(i).get());
+        for (Future<BasicStats> future : futures) {
+          ret.add(future.get());
         }
       } catch (InterruptedException | ExecutionException e) {
         LOG.warn("Exception in processing files ", e);
@@ -219,6 +210,7 @@ private long getFileSizeForPath(Path path) throws 
IOException {
   }
 
   private Partish partish;
+  private Map<String, String> params;
 
   private long rowCount;
   private long totalSize;
@@ -230,8 +222,20 @@ private long getFileSizeForPath(Path path) throws 
IOException {
   private Statistics.State state;
 
   public BasicStats(Partish p) {
+    this(p, null);
+  }
+
+  /**
+   * Creates the basic stats for the given partish.
+   *
+   * @param p the table/partition wrapper object
+   * @param providedStats pre-fetched basic statistics, e.g. obtained from the 
storage handler in batch via
+   *     {@link 
org.apache.hadoop.hive.ql.metadata.HiveStorageHandler#getAggrBasicStatsFor};
+   *     when null, the stats are read from the partish parameters
+   */
+  public BasicStats(Partish p, Map<String, String> providedStats) {
     partish = p;
-    tryGetBasicStatsFromStorageHandler();
+    params = providedStats != null ? providedStats : 
partish.getPartParameters();
 
     rowCount = parseLong(StatsSetupConst.ROW_COUNT);
     rawDataSize = parseLong(StatsSetupConst.RAW_DATA_SIZE);
@@ -272,18 +276,17 @@ public BasicStats(List<BasicStats> partStats) {
 
   }
 
-  private void tryGetBasicStatsFromStorageHandler() {
-    if (partish.getTable() != null && partish.getTable().isNonNative() &&
-        partish.getTable().getStorageHandler().canProvideBasicStatistics()) {
-      partish.getPartParameters().putAll(
-          partish.getTable().getStorageHandler().getBasicStatistics(partish));
-    }
-  }
-
   public long getNumRows() {
     return currentNumRows;
   }
 
+  /**
+   * @return the row count parsed from the stats source, before any 
enhancer-based estimation
+   */
+  public long getRawNumRows() {
+    return rowCount;
+  }
+
   public long getDataSize() {
     return currentDataSize;
   }
@@ -317,7 +320,6 @@ protected long getRawDataSize() {
   }
 
   private long parseLong(String fieldName) {
-    Map<String, String> params = partish.getPartParameters();
     long result = -1;
 
     if (params != null) {
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsNoJobTask.java 
b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsNoJobTask.java
index defcd586098..24305b1ffb7 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsNoJobTask.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsNoJobTask.java
@@ -174,17 +174,22 @@ public HiveStorageHandlerStatCollector(Partish partish) {
     public void run() {
       try {
         Table table = partish.getTable();
-        Map<String, String> parameters;
-        Map<String, String> basicStatistics = 
table.getStorageHandler().getBasicStatistics(partish);
         if (partish.getPartition() != null) {
-          parameters = partish.getPartParameters();
-          result = new Partition(table, 
partish.getPartition().getTPartition());
-        } else {
-          parameters = table.getParameters();
-          result = new Table(table.getTTable());
+          // the storage handler maintains the statistics of all partitions as 
a whole; a partition-scoped
+          // ANALYZE is rejected at compile time 
(ErrorMsg.ANALYZE_PARTITION_NON_NATIVE)
+          throw new IllegalStateException("Partition-scoped statistics 
collection is not supported for "
+              + table.getFullyQualifiedName());
+        }
+        Map<String, String> parameters = table.getParameters();
+        // computes and publishes the statistics the storage handler maintains 
(e.g. the Iceberg
+        // partition stats file) before reading them back: ANALYZE catches up 
on writes that
+        // skipped the auto-gathering (a no-op when the statistics are already 
current)
+        Map<String, String> basicStatistics = 
table.getStorageHandler().computeBasicStatistics(table);
+        result = new Table(table.getTTable());
+        if (basicStatistics != null) {
+          parameters.putAll(basicStatistics);
+          StatsSetupConst.setBasicStatsState(parameters, StatsSetupConst.TRUE);
         }
-        parameters.putAll(basicStatistics);
-        StatsSetupConst.setBasicStatsState(parameters, StatsSetupConst.TRUE);
 
         String who = (partish.getPartition() == null) ? ("table " + 
partish.getTable().getFullyQualifiedName())
                 : ("partition " + partish.getPartition().getName());
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsTask.java 
b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsTask.java
index 722b2f70119..41d152a49e8 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsTask.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/stats/BasicStatsTask.java
@@ -138,8 +138,11 @@ public BasicStatsProcessor(Partish partish, BasicStatsWork 
work, boolean followe
       this.followedColStats1 = followedColStats2;
       
       Table table = partish.getTable();
-      if (table.isNonNative() && 
table.getStorageHandler().canProvideBasicStatistics()) {
-        this.providedBasicStats = 
table.getStorageHandler().computeBasicStatistics(partish);
+      // table scope only: a partition-scoped ANALYZE is rejected at compile 
time for tables with
+      // non-native partitioning (ErrorMsg.ANALYZE_PARTITION_NON_NATIVE)
+      if (table.isNonNative() && 
table.getStorageHandler().canProvideBasicStatistics()
+          && partish.getPartition() == null) {
+        this.providedBasicStats = 
table.getStorageHandler().computeBasicStatistics(table);
         this.skipStatsUpdate = StatsSetupConst.STATS_REQUIRE_COMPUTE.stream()
             .anyMatch(providedBasicStats::containsKey);
       }
@@ -515,7 +518,7 @@ private List<Partition> getPartitionsList(Hive db) throws 
HiveException {
       // ANALYZE command
       TableSpec tblSpec = work.getTableSpecs();
       table = tblSpec.tableHandle;
-      if (!table.isPartitioned()) {
+      if (!table.isPartitioned() || table.hasNonNativePartitionSupport()) {
         return null;
       }
       // get all partitions that match with the partition spec
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java 
b/ql/src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java
index e539f0ab9c4..dbb601d4c99 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/stats/StatsUtils.java
@@ -35,9 +35,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
-import java.util.concurrent.ForkJoinPool;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.hadoop.fs.FileSystem;
@@ -63,6 +61,7 @@
 import org.apache.hadoop.hive.ql.exec.Utilities;
 import org.apache.hadoop.hive.ql.metadata.Hive;
 import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.ql.metadata.HiveStorageHandler;
 import org.apache.hadoop.hive.ql.metadata.Partition;
 import org.apache.hadoop.hive.ql.metadata.PartitionIterable;
 import org.apache.hadoop.hive.ql.metadata.Table;
@@ -89,7 +88,6 @@
 import org.apache.hadoop.hive.ql.udf.generic.GenericUDFBridge;
 import org.apache.hadoop.hive.ql.udf.generic.NDV;
 import org.apache.hadoop.hive.ql.util.JavaDataModel;
-import org.apache.hadoop.hive.ql.util.NamedForkJoinWorkerThreadFactory;
 import org.apache.hadoop.hive.serde.serdeConstants;
 import org.apache.hadoop.hive.serde2.objectinspector.ConstantObjectInspector;
 import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
@@ -151,17 +149,6 @@ public class StatsUtils {
   // Range upper limit for timestamp type when not defined (seconds, 
heuristic): '2030-12-31 23:59:59'
   private static final long TIMESTAMP_RANGE_UPPER_LIMIT = 1924991999L;
 
-  private static final ForkJoinPool statsForkJoinPool = new ForkJoinPool(
-          Runtime.getRuntime().availableProcessors(),
-          new NamedForkJoinWorkerThreadFactory("basic-stats-"),
-          getUncaughtExceptionHandler(),
-          false
-  );
-
-  private static Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() 
{
-    return (t, e) -> LOG.error(String.format("Thread %s exited with error", 
t.getName()), e);
-  }
-
   /**
    * Collect table, partition and column level statistics
    * @param conf
@@ -175,7 +162,8 @@ private static Thread.UncaughtExceptionHandler 
getUncaughtExceptionHandler() {
    * @return statistics object
    * @throws HiveException
    */
-  public static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList partList, ColumnStatsList colStatsCache,
+  public static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList partList,
+      ColumnStatsList colStatsCache,
       Table table, TableScanOperator tableScanOperator) throws HiveException {
 
     // column level statistics are required only for the columns that are 
needed
@@ -183,11 +171,13 @@ public static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList pa
     List<String> neededColumns = tableScanOperator.getNeededColumns();
     List<String> referencedColumns = tableScanOperator.getReferencedColumns();
 
-    return collectStatistics(conf, partList, table, schema, neededColumns, 
colStatsCache, referencedColumns);
+    return collectStatistics(conf, partList, table, schema, neededColumns, 
colStatsCache,
+        referencedColumns);
   }
 
   private static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList partList,
-      Table table, List<ColumnInfo> schema, List<String> neededColumns, 
ColumnStatsList colStatsCache,
+      Table table, List<ColumnInfo> schema, List<String> neededColumns,
+      ColumnStatsList colStatsCache,
       List<String> referencedColumns) throws HiveException {
 
     boolean fetchColStats =
@@ -195,8 +185,8 @@ private static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList p
     boolean testMode =
         HiveConf.getBoolVar(conf, HiveConf.ConfVars.HIVE_IN_TEST);
 
-    return collectStatistics(conf, partList, table, schema, neededColumns, 
colStatsCache, referencedColumns,
-        fetchColStats, testMode);
+    return collectStatistics(conf, partList, table, schema, neededColumns, 
colStatsCache,
+        referencedColumns, fetchColStats, testMode);
   }
 
   /**
@@ -207,38 +197,73 @@ private static Statistics collectStatistics(HiveConf 
conf, PrunedPartitionList p
   public static long getNumRows(HiveConf conf, List<ColumnInfo> schema, Table 
table, PrunedPartitionList partitionList, 
       AtomicInteger noColsMissingStats) {
 
-    List<Partish> inputs = new ArrayList<>();
-    if (table.isPartitioned()) {
-      for (Partition part : partitionList.getNotDeniedPartns()) {
-        inputs.add(Partish.buildFor(table, part));
-      }
-    } else {
-      inputs.add(Partish.buildFor(table));
-    }
-
     Factory basicStatsFactory = new BasicStats.Factory();
 
     if (HiveConf.getBoolVar(conf, ConfVars.HIVE_STATS_ESTIMATE_STATS)) {
       basicStatsFactory.addEnhancer(new BasicStats.DataSizeEstimator(conf));
       basicStatsFactory.addEnhancer(new 
BasicStats.RowNumEstimator(estimateRowSizeFromSchema(conf, schema)));
     }
-    
-    for (Partish pi : inputs) {
-      BasicStats bStats = new BasicStats(pi);
-      long nr = bStats.getNumRows();
-      // FIXME: this point will be lost after the factory; check that it's 
really a warning....cleanup/etc
-      if (nr <= 0) {
-        // log warning if row count is missing
-        noColsMissingStats.getAndIncrement();
-      }
+
+    // when partition-level statistics are unavailable (e.g. non-native table 
with an external stats
+    // source) fall back to the table-level statistics rather than 
per-partition minimums
+    List<BasicStats> results;
+    if (table.isPartitioned() && checkCanProvidePartitionStats(table)) {
+      results = buildPartitionStats(conf, table, partitionList, 
basicStatsFactory);
+    } else {
+      results = List.of(buildTableStats(table, basicStatsFactory));
     }
-    List<BasicStats> results = basicStatsFactory.buildAll(conf, inputs);
+    // count the entries with missing row counts (estimated rows do not count 
as provided)
+    noColsMissingStats.addAndGet((int) results.stream()
+        .filter(bStats -> bStats.getRawNumRows() <= 0)
+        .count());
     BasicStats aggregateStat = BasicStats.buildFrom(results);
 
     aggregateStat.apply(new BasicStats.SetMinRowNumber());
     return aggregateStat.getNumRows();
   }
 
+  /**
+   * Builds the per-partition basic stats. When the storage handler provides 
them, one batched read
+   * (see {@link HiveStorageHandler#getAggrBasicStatsFor}) serves the whole 
partition list; otherwise each
+   * partition is read individually (see {@link BasicStats.Factory#buildAll}).
+   */
+  private static List<BasicStats> buildPartitionStats(HiveConf conf, Table 
table, PrunedPartitionList partList,
+      BasicStats.Factory factory) {
+    List<Partish> inputs = partList.getNotDeniedPartns().stream()
+        .map(part -> Partish.buildFor(table, part))
+        .toList();
+    HiveStorageHandler storageHandler = table.isNonNative() ? 
table.getStorageHandler() : null;
+    if (storageHandler != null && storageHandler.canProvideBasicStatistics()) {
+      if (partList.getReferredPartCols().isEmpty() && !inputs.isEmpty()) {
+        // no partition predicate: a non-empty list covers every partition, so 
the table-level statistics
+        // are exactly their aggregate - skip the per-partition read
+        return List.of(buildTableStats(table, factory));
+      }
+      List<String> partNames = inputs.stream()
+          .map(partish -> partish.getPartition().getName())
+          .toList();
+      Map<String, Map<String, String>> aggrBasicStats = partNames.isEmpty() ? 
Map.of() :
+          storageHandler.getAggrBasicStatsFor(table, partNames);
+      if (!aggrBasicStats.isEmpty()) {
+        return inputs.stream()
+            .map(pi -> factory.build(pi,
+                aggrBasicStats.getOrDefault(pi.getPartition().getName(), 
Map.of())))
+            .toList();
+      }
+    }
+    return factory.buildAll(conf, inputs);
+  }
+
+  /**
+   * Builds the table-level basic stats, sourced from the storage handler when 
it provides them.
+   */
+  private static BasicStats buildTableStats(Table table, BasicStats.Factory 
factory) {
+    HiveStorageHandler storageHandler = table.isNonNative() ? 
table.getStorageHandler() : null;
+    Map<String, String> providedStats = storageHandler != null && 
storageHandler.canProvideBasicStatistics() ?
+        storageHandler.getBasicStatistics(table) : null;
+    return factory.build(Partish.buildFor(table), providedStats);
+  }
+
   /**
    * Estimates column statistics for columns specified in {@code 
neededColumnNames}
    * that do not already have statistics in the {@code existingColStats} list.
@@ -269,7 +294,8 @@ static List<ColStatistics> estimateStatsForMissingCols(
   }
 
   public static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList partList,
-      Table table, List<ColumnInfo> schema, List<String> neededColumns, 
ColumnStatsList colStatsCache,
+      Table table, List<ColumnInfo> schema, List<String> neededColumns,
+      ColumnStatsList colStatsCache,
       List<String> referencedColumns, boolean needColStats)
       throws HiveException {
     return collectStatistics(conf, partList, table, schema, neededColumns, 
colStatsCache,
@@ -277,7 +303,8 @@ public static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList pa
   }
 
   private static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList partList, Table table,
-      List<ColumnInfo> schema, List<String> neededColumns, ColumnStatsList 
colStatsCache,
+      List<ColumnInfo> schema, List<String> neededColumns,
+      ColumnStatsList colStatsCache,
       List<String> referencedColumns, boolean needColStats, boolean 
failIfCacheMiss) throws HiveException {
 
     Statistics stats = null;
@@ -298,7 +325,7 @@ private static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList p
       basicStatsFactory.addEnhancer(new 
BasicStats.RowNumEstimator(estimateRowSizeFromSchema(conf, schema)));
       basicStatsFactory.addEnhancer(new BasicStats.SetMinRowNumber());
 
-      BasicStats basicStats = basicStatsFactory.build(Partish.buildFor(table));
+      BasicStats basicStats = buildTableStats(table, basicStatsFactory);
       
       //      long nr = getNumRows(conf, schema, neededColumns, table, ds);
       long ds = basicStats.getDataSize();
@@ -339,16 +366,7 @@ private static Statistics collectStatistics(HiveConf conf, 
PrunedPartitionList p
 
       basicStatsFactory.addEnhancer(new 
BasicStats.RowNumEstimator(estimateRowSizeFromSchema(conf, schema)));
 
-      List<BasicStats> partStats = null;
-      try {
-        partStats = statsForkJoinPool.submit(() ->
-          partList.getNotDeniedPartns().parallelStream().
-                  map(p -> basicStatsFactory.build(Partish.buildFor(table, 
p))).
-                  collect(Collectors.toList())
-        ).get();
-      } catch (Exception e) {
-        throw new HiveException(e);
-      }
+      List<BasicStats> partStats = buildPartitionStats(conf, table, partList, 
basicStatsFactory);
 
       BasicStats bbs = BasicStats.buildFrom(partStats);
 
diff --git a/ql/src/test/org/apache/hadoop/hive/ql/stats/TestBasicStats.java 
b/ql/src/test/org/apache/hadoop/hive/ql/stats/TestBasicStats.java
index cb98259dbce..9fba3eeaac8 100644
--- a/ql/src/test/org/apache/hadoop/hive/ql/stats/TestBasicStats.java
+++ b/ql/src/test/org/apache/hadoop/hive/ql/stats/TestBasicStats.java
@@ -29,7 +29,6 @@
 import org.apache.hadoop.hive.conf.HiveConf.ConfVars;
 import org.apache.hadoop.hive.ql.metadata.Partition;
 import org.apache.hadoop.hive.ql.plan.Statistics.State;
-import org.apache.hadoop.hive.ql.stats.BasicStats;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mockito;
@@ -65,6 +64,33 @@ public Partish buildPartition() {
     }
   }
 
+  @Test
+  public void testProvidedStatsTakePrecedenceOverPartitionParameters() {
+    Map<String, String> partParams = new HashMap<>();
+    partParams.put(StatsSetupConst.ROW_COUNT, "7");
+    Partish partish = partitionPartish(partParams);
+
+    // Stats provided explicitly (e.g. batched via 
HiveStorageHandler#getAggrBasicStatsFor):
+    // used as-is, no partition parameter reads or mutation.
+    Map<String, String> providedStats = new HashMap<>();
+    providedStats.put(StatsSetupConst.ROW_COUNT, "42");
+
+    assertEquals(42, new BasicStats(partish, providedStats).getNumRows());
+    assertEquals("7", partParams.get(StatsSetupConst.ROW_COUNT));
+
+    // Without provided stats, the partition parameters are read.
+    assertEquals(7, new BasicStats(partish).getNumRows());
+  }
+
+  private static Partish partitionPartish(Map<String, String> params) {
+    Partition partition = Mockito.mock(Partition.class);
+    org.apache.hadoop.hive.metastore.api.Partition tPartition =
+        Mockito.mock(org.apache.hadoop.hive.metastore.api.Partition.class);
+    doReturn(tPartition).when(partition).getTPartition();
+    doReturn(params).when(tPartition).getParameters();
+    return Partish.buildFor(null, partition);
+  }
+
   @Test
   public void testDataSizeEstimator() {
     Partish p1 = new LocalPartishBuilder().totalSize(10).buildPartition();

Reply via email to