This is an automated email from the ASF dual-hosted git repository.
dengzhhu653 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 6d11793e53b HIVE-28755: Statistics Management Task (#6438)
6d11793e53b is described below
commit 6d11793e53b6d22e1b36b0759a3dbdc35962e9ea
Author: Daniel (Hongdan) Zhu <[email protected]>
AuthorDate: Wed May 13 19:41:33 2026 -0700
HIVE-28755: Statistics Management Task (#6438)
---
.../hadoop/hive/metastore/conf/MetastoreConf.java | 17 +-
.../hive/metastore/StatisticsManagementTask.java | 277 +++++++++++
.../hive/metastore/TestStatisticsManagement.java | 543 +++++++++++++++++++++
3 files changed, 836 insertions(+), 1 deletion(-)
diff --git
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
index 8f8befd8348..cae543f0b8d 100644
---
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
+++
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
@@ -1291,6 +1291,20 @@ public enum ConfVars {
"metastore.partition.management.table.pattern", "*",
"Automatic partition management will look for tables using the specified
table pattern"),
+
COLUMN_STATISTICS_MANAGEMENT_TASK_FREQUENCY("metastore.column.statistics.management.task.frequency",
+ "metastore.column.statistics.management.task.frequency",
+ 0, TimeUnit.DAYS,
+ "Frequency at which the statistics management task runs to
automatically delete expired " +
+ "column statistics. A value of 0 (the default) disables the task
entirely. " +
+ "Set to a positive duration (e.g. 7d) to enable periodic deletion.
" +
+ "Rows whose lastAnalyzed timestamp is older than " +
+ "'metastore.column.statistics.retention.period' will be deleted."),
+
+
COLUMN_STATISTICS_RETENTION_PERIOD("metastore.column.statistics.retention.period",
+ "metastore.column.statistics.retention.period", 365, TimeUnit.DAYS,
+ "The retention period for column statistics. Statistics older than
this period " +
+ "will be deleted when the statistics management task is enabled."),
+
METASTORE_METADATA_TRANSFORMER_CLASS("metastore.metadata.transformer.class",
"metastore.metadata.transformer.class",
"org.apache.hadoop.hive.metastore.MetastoreDefaultTransformer",
"Fully qualified class name for the metastore metadata transformer
class \n"
@@ -1526,7 +1540,8 @@ public enum ConfVars {
ACID_METRICS_TASK_CLASS + "," + ACID_METRICS_LOGGER_CLASS + "," +
"org.apache.hadoop.hive.metastore.HiveProtoEventsCleanerTask" + ","
+
"org.apache.hadoop.hive.metastore.ScheduledQueryExecutionsMaintTask" + ","
- + "org.apache.hadoop.hive.metastore.ReplicationMetricsMaintTask",
+ + "org.apache.hadoop.hive.metastore.ReplicationMetricsMaintTask" +
","
+ + "org.apache.hadoop.hive.metastore.StatisticsManagementTask",
"Comma separated list of tasks that will be started in separate
threads. These will " +
"always be started, regardless of whether the metastore is running
in embedded mode " +
"or in server mode. They must implement " +
METASTORE_TASK_THREAD_CLASS),
diff --git
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/StatisticsManagementTask.java
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/StatisticsManagementTask.java
new file mode 100644
index 00000000000..5bd6dc6b146
--- /dev/null
+++
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/StatisticsManagementTask.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.hive.metastore;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import org.apache.hadoop.hive.metastore.model.MPartitionColumnStatistics;
+import org.apache.hadoop.hive.metastore.model.MTableColumnStatistics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jdo.PersistenceManager;
+import javax.jdo.Query;
+
+/**
+ * Statistics management task responsible for periodic auto-deletion of table
and partition column
+ * statistics based on a configured retention interval.
+ *
+ * <p>When {@code metastore.column.statistics.auto.deletion} is enabled, this
task scans
+ * {@code TAB_COL_STATS} and {@code PART_COL_STATS} for rows whose {@code
lastAnalyzed} timestamp
+ * is older than {@code metastore.column.statistics.retention.period}, and
deletes them.
+ * Individual tables may opt out by setting the table property
+ * {@value #STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY} to {@code "true"}.
+ */
+public class StatisticsManagementTask extends ObjectStore implements
MetastoreTaskThread {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(StatisticsManagementTask.class);
+
+ /**
+ * Table property key that, when set to {@code "true"} on a table, excludes
it from automatic
+ * statistics deletion regardless of the global retention setting.
+ */
+ public static final String STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY =
+ "statistics.auto.deletion.exclude";
+
+ /** Separator used when building composite map keys; chosen to be safe in
HMS identifiers. */
+ private static final String KEY_SEP = "\0";
+
+ private static final Lock LOCK = new ReentrantLock();
+
+ @Override
+ public long runFrequency(TimeUnit unit) {
+ // when frequency=0, the auto deletion task is not being run
+ if (MetastoreConf.getTimeVar(conf,
MetastoreConf.ConfVars.COLUMN_STATISTICS_RETENTION_PERIOD,
+ TimeUnit.MILLISECONDS) <= 0) {
+ return 0;
+ }
+ return MetastoreConf.getTimeVar(conf,
+ MetastoreConf.ConfVars.COLUMN_STATISTICS_MANAGEMENT_TASK_FREQUENCY,
unit);
+ }
+
+ @Override
+ public void setConf(Configuration configuration) {
+ this.conf = new Configuration(configuration);
+ super.setConf(this.conf);
+ }
+
+ @Override
+ public Configuration getConf() {
+ return conf;
+ }
+
+ @Override
+ public void run() {
+ LOG.debug("Auto statistics deletion started. Cleaning up table/partition
column statistics"
+ + " over the retention period.");
+ long retentionMillis = MetastoreConf.getTimeVar(
+ conf, MetastoreConf.ConfVars.COLUMN_STATISTICS_RETENTION_PERIOD,
TimeUnit.MILLISECONDS);
+ if (retentionMillis <= 0) {
+ LOG.info("Statistics auto deletion is set to off currently.");
+ return;
+ }
+ if (!LOCK.tryLock()) {
+ return;
+ }
+ try {
+ long now = System.currentTimeMillis();
+ long lastAnalyzedThreshold = (now - retentionMillis) / 1000;
+ List<Object[]> expiredTblRows;
+ List<Object[]> expiredPartRows;
+ boolean committed = false;
+ openTransaction();
+ try {
+ PersistenceManager pm = getPersistenceManager();
+ expiredTblRows = collectExpiredTableColStats(pm,
lastAnalyzedThreshold);
+ expiredPartRows = collectExpiredPartitionColStats(pm,
lastAnalyzedThreshold);
+ committed = commitTransaction();
+ } finally {
+ if (!committed) {
+ rollbackTransaction();
+ }
+ }
+ deleteExpiredTableColStats(expiredTblRows);
+ deleteExpiredPartitionColStats(expiredPartRows);
+ } catch (Exception e) {
+ LOG.error("Error during statistics auto deletion", e);
+ } finally {
+ LOCK.unlock();
+ }
+ }
+
+ /**
+ * Queries {@code TAB_COL_STATS} for rows whose {@code lastAnalyzed} is
older than the given
+ * threshold. Results are copied into an {@link ArrayList} so they remain
accessible after
+ * the enclosing JDO transaction is committed.
+ *
+ * @param pm the JDO persistence manager to use for the
query
+ * @param lastAnalyzedThreshold epoch seconds; rows with lastAnalyzed below
this value are expired
+ * @return list of projected rows: [catName, dbName, tblName, colName]
+ * @throws Exception if the JDO query fails
+ */
+ private List<Object[]> collectExpiredTableColStats(PersistenceManager pm,
+ long
lastAnalyzedThreshold) throws Exception {
+ try (Query tblQuery = pm.newQuery(MTableColumnStatistics.class)) {
+ tblQuery.setFilter("lastAnalyzed < threshold && engine == \"hive\"");
+ tblQuery.declareParameters("long threshold");
+ tblQuery.setRange(0, 1000);
+ tblQuery.setResult(
+ "table.database.catalogName, "
+ + "table.database.name, "
+ + "table.tableName, "
+ + "colName, "
+ + "table.parameters.get(\"" +
STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY + "\")");
+ @SuppressWarnings("unchecked")
+ List<Object[]> rows = (List<Object[]>)
tblQuery.execute(lastAnalyzedThreshold);
+ return new ArrayList<>(rows);
+ }
+ }
+
+ /**
+ * Groups expired table-level column stats by {@code (catName, dbName,
tblName)}, then deletes
+ * all expired columns for each table in a single {@link RawStore} call and
a dedicated
+ * transaction. This avoids duplicate listener events for the same table.
+ * Tables whose exclude property is set to {@code "true"} are skipped
entirely.
+ *
+ * @param expiredTblRows projected rows from {@link
#collectExpiredTableColStats}
+ * @throws Exception if a delete operation fails
+ */
+ private void deleteExpiredTableColStats(List<Object[]> expiredTblRows)
throws Exception {
+ // key: catName + SEP + dbName + SEP + tblName, value: list of expired
colNames
+ Map<String, List<String>> tblToColsMap = new LinkedHashMap<>();
+ // keep a parallel map to reconstruct the key parts when issuing deletes
+ Map<String, String[]> keyToCoords = new LinkedHashMap<>();
+
+ for (Object[] row : expiredTblRows) {
+ String catName = (String) row[0];
+ String dbName = (String) row[1];
+ String tblName = (String) row[2];
+ String colName = (String) row[3];
+ String excludeVal = (String) row[4];
+ if (Boolean.parseBoolean(excludeVal)) {
+ LOG.info("Skipping auto deletion of table stats for {}.{} due to
exclude property.",
+ dbName, tblName);
+ continue;
+ }
+ String key = catName + KEY_SEP + dbName + KEY_SEP + tblName;
+ tblToColsMap.computeIfAbsent(key, k -> new ArrayList<>()).add(colName);
+ keyToCoords.putIfAbsent(key, new String[]{catName, dbName, tblName});
+ }
+
+ // one transaction per table, delete all expired columns in a single call
+ for (Map.Entry<String, List<String>> entry : tblToColsMap.entrySet()) {
+ String[] coords = keyToCoords.get(entry.getKey());
+ boolean committed = false;
+ openTransaction();
+ try {
+ deleteTableColumnStatistics(coords[0], coords[1], coords[2],
entry.getValue(), "hive");
+ committed = commitTransaction();
+ } finally {
+ if (!committed) {
+ rollbackTransaction();
+ }
+ }
+ }
+ }
+
+ /**
+ * Queries {@code PART_COL_STATS} for rows whose {@code lastAnalyzed} is
older than the given
+ * threshold. Results are copied into an {@link ArrayList} so they remain
accessible after
+ * the enclosing JDO transaction is committed.
+ *
+ * @param pm the JDO persistence manager to use for the
query
+ * @param lastAnalyzedThreshold epoch seconds; rows with lastAnalyzed below
this value are expired
+ * @return list of projected rows: [catName, dbName, tblName, partName,
colName]
+ * @throws Exception if the JDO query fails
+ */
+ private List<Object[]> collectExpiredPartitionColStats(PersistenceManager pm,
+ long
lastAnalyzedThreshold) throws Exception {
+ try (Query partQuery = pm.newQuery(MPartitionColumnStatistics.class)) {
+ partQuery.setFilter("lastAnalyzed < threshold && engine == \"hive\"");
+ partQuery.declareParameters("long threshold");
+ partQuery.setRange(0, 1000);
+ partQuery.setResult(
+ "partition.table.database.catalogName, "
+ + "partition.table.database.name, "
+ + "partition.table.tableName, "
+ + "partition.partitionName, "
+ + "colName, "
+ + "partition.table.parameters.get(\"" +
STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY + "\")");
+ @SuppressWarnings("unchecked")
+ List<Object[]> rows = (List<Object[]>)
partQuery.execute(lastAnalyzedThreshold);
+ return new ArrayList<>(rows);
+ }
+ }
+
+ /**
+ * Groups expired partition-level column stats by {@code (catName, dbName,
tblName, partName)},
+ * then deletes all expired columns for each partition in a single {@link
RawStore} call and a
+ * dedicated transaction. This avoids duplicate listener events for the same
partition.
+ * Tables whose exclude property is set to {@code "true"} are skipped
entirely.
+ *
+ * @param expiredPartRows projected rows from {@link
#collectExpiredPartitionColStats}
+ * @throws Exception if a delete operation fails
+ */
+ private void deleteExpiredPartitionColStats(List<Object[]> expiredPartRows)
throws Exception {
+ // key: catName + SEP + dbName + SEP + tblName + SEP + partName, value:
list of expired colNames
+ Map<String, List<String>> partToColsMap = new LinkedHashMap<>();
+ Map<String, String[]> keyToCoords = new LinkedHashMap<>();
+ for (Object[] row : expiredPartRows) {
+ String catName = (String) row[0];
+ String dbName = (String) row[1];
+ String tblName = (String) row[2];
+ String partName = (String) row[3];
+ String colName = (String) row[4];
+ String excludeVal = (String) row[5];
+ if (Boolean.parseBoolean(excludeVal)) {
+ LOG.info("Skipping auto deletion of partition stats for {}.{} due to
exclude property.",
+ dbName, tblName);
+ continue;
+ }
+ String key = catName + KEY_SEP + dbName + KEY_SEP + tblName + KEY_SEP +
partName;
+ partToColsMap.computeIfAbsent(key, k -> new ArrayList<>()).add(colName);
+ keyToCoords.putIfAbsent(key, new String[]{catName, dbName, tblName,
partName});
+ }
+
+ // one transaction per partition, delete all expired columns in a single
call
+ for (Map.Entry<String, List<String>> entry : partToColsMap.entrySet()) {
+ String[] coords = keyToCoords.get(entry.getKey());
+ boolean committed = false;
+ openTransaction();
+ try {
+ deletePartitionColumnStatistics(coords[0], coords[1], coords[2],
+ Collections.singletonList(coords[3]), entry.getValue(), "hive");
+ committed = commitTransaction();
+ } finally {
+ if (!committed) {
+ rollbackTransaction();
+ }
+ }
+ }
+ }
+}
diff --git
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestStatisticsManagement.java
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestStatisticsManagement.java
new file mode 100644
index 00000000000..53aac567acb
--- /dev/null
+++
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestStatisticsManagement.java
@@ -0,0 +1,543 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hive.metastore;
+
+import static org.apache.hadoop.hive.metastore.Warehouse.DEFAULT_CATALOG_NAME;
+import static org.apache.hadoop.hive.metastore.Warehouse.DEFAULT_DATABASE_NAME;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.annotation.MetastoreUnitTest;
+import org.apache.hadoop.hive.metastore.api.ColumnStatistics;
+import org.apache.hadoop.hive.metastore.api.ColumnStatisticsData;
+import org.apache.hadoop.hive.metastore.api.ColumnStatisticsDesc;
+import org.apache.hadoop.hive.metastore.api.ColumnStatisticsObj;
+import org.apache.hadoop.hive.metastore.api.Database;
+import org.apache.hadoop.hive.metastore.api.DoubleColumnStatsData;
+import org.apache.hadoop.hive.metastore.api.NoSuchObjectException;
+import org.apache.hadoop.hive.metastore.api.Partition;
+import org.apache.hadoop.hive.metastore.api.Table;
+import org.apache.hadoop.hive.metastore.client.builder.DatabaseBuilder;
+import org.apache.hadoop.hive.metastore.client.builder.PartitionBuilder;
+import org.apache.hadoop.hive.metastore.client.builder.TableBuilder;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf.ConfVars;
+import org.apache.hadoop.hive.metastore.model.MPartitionColumnStatistics;
+import org.apache.hadoop.hive.metastore.model.MTableColumnStatistics;
+import org.apache.hadoop.hive.metastore.security.HadoopThriftAuthBridge;
+import org.apache.hadoop.hive.metastore.utils.TestTxnDbUtil;
+import org.apache.thrift.TException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import javax.jdo.PersistenceManager;
+import javax.jdo.Query;
+
+/**
+ * Unit tests for {@link StatisticsManagementTask}, verifying that expired
table-level and
+ * partition-level column statistics are deleted on schedule, and that tables
marked with the
+ * exclude property are left untouched.
+ */
+@Category(MetastoreUnitTest.class)
+public class TestStatisticsManagement {
+
+ private IMetaStoreClient client;
+ private Configuration conf;
+
+ @Before
+ public void setUp() throws Exception {
+ conf = MetastoreConf.newMetastoreConf();
+ MetastoreConf.setVar(conf, ConfVars.METASTORE_METADATA_TRANSFORMER_CLASS,
" ");
+ MetaStoreTestUtils.setConfForStandloneMode(conf);
+ conf.setBoolean(ConfVars.MULTITHREADED.getVarname(), false);
+ conf.setBoolean(ConfVars.HIVE_IN_TEST.getVarname(), true);
+
+ // Enable stats auto deletion with a short retention so the threshold
check triggers easily.
+ MetastoreConf.setTimeVar(conf,
ConfVars.COLUMN_STATISTICS_MANAGEMENT_TASK_FREQUENCY, 1, TimeUnit.DAYS);
+ MetastoreConf.setTimeVar(conf,
ConfVars.COLUMN_STATISTICS_RETENTION_PERIOD, 1, TimeUnit.DAYS);
+
+
MetaStoreTestUtils.startMetaStoreWithRetry(HadoopThriftAuthBridge.getBridge(),
conf);
+ TestTxnDbUtil.setConfValues(conf);
+ TestTxnDbUtil.prepDb(conf);
+
+ client = new HiveMetaStoreClient(conf);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (client != null) {
+ // Drop any leftover databases, similar to TestPartitionManagement.java.
+ List<String> dbs = client.getAllDatabases(DEFAULT_CATALOG_NAME);
+ for (String db : dbs) {
+ if (!db.equalsIgnoreCase(DEFAULT_DATABASE_NAME)) {
+ client.dropDatabase(DEFAULT_CATALOG_NAME, db, true, false, true);
+ }
+ }
+ }
+ try {
+ if (client != null) {
+ client.close();
+ }
+ } finally {
+ client = null;
+ }
+ }
+
+ @Test
+ public void testExpiredTableColStatsAreDeleted() throws Exception {
+ String dbName = "stats_db1";
+ String tableName = "tbl1";
+ createDbAndTable(dbName, tableName, false);
+ writeTableLevelColStats(dbName, tableName, "c1");
+ assertHasTableColStats(dbName, tableName, "c1");
+ makeAllTableColStatsOlderThanRetention(dbName, tableName);
+
+ runStatisticsManagementTask(conf);
+
+ assertNoTableColStats(dbName, tableName, "c1");
+ }
+
+ @Test
+ public void testExcludedTableStatsAreNotDeleted() throws Exception {
+ String dbName = "stats_db2";
+ String tableName = "tbl2";
+ createDbAndTable(dbName, tableName, true);
+ writeTableLevelColStats(dbName, tableName, "c1");
+ assertHasTableColStats(dbName, tableName, "c1");
+ makeAllTableColStatsOlderThanRetention(dbName, tableName);
+
+ runStatisticsManagementTask(conf);
+
+ // Stats must still be present because the table is marked as excluded.
+ assertHasTableColStats(dbName, tableName, "c1");
+ }
+
+ /**
+ * Verifies that setting {@value
StatisticsManagementTask#STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY}
+ * to {@code "false"} does not prevent deletion. Only an explicit {@code
"true"} value excludes
+ * a table; any other value (including {@code "false"}) is treated as not
excluded.
+ */
+ @Test
+ public void testExcludePropertySetToFalseDoesNotPreventDeletion() throws
Exception {
+ String dbName = "stats_db3";
+ String tableName = "tbl3";
+ createDbAndTable(dbName, tableName, false);
+
+ // Explicitly set the exclude property to "false" — the task must still
delete these stats.
+ Table t = client.getTable(dbName, tableName);
+ t.getParameters().put(
+ StatisticsManagementTask.STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY,
"false");
+ client.alter_table(dbName, tableName, t);
+
+ writeTableLevelColStats(dbName, tableName, "c1");
+ assertHasTableColStats(dbName, tableName, "c1");
+ makeAllTableColStatsOlderThanRetention(dbName, tableName);
+
+ runStatisticsManagementTask(conf);
+
+ // "false" is not a valid exclude signal; stats should be deleted.
+ assertNoTableColStats(dbName, tableName, "c1");
+ }
+
+ /**
+ * Verifies that expired partition-level column statistics are deleted when
the task runs.
+ */
+ @Test
+ public void testExpiredPartitionColStatsAreDeleted() throws Exception {
+ String dbName = "stats_db4";
+ String tableName = "tbl4";
+ String partVal = "p1";
+ createDbAndPartitionedTable(dbName, tableName, false);
+ createPartition(dbName, tableName, partVal);
+
+ String partName = "part_col=" + partVal;
+ writePartitionLevelColStats(dbName, tableName, partName, "c1");
+ assertHasPartitionColStats(dbName, tableName, partName, "c1");
+ makeAllPartitionColStatsOlderThanRetention(dbName, tableName);
+
+ runStatisticsManagementTask(conf);
+
+ assertNoPartitionColStats(dbName, tableName, partName, "c1");
+ }
+
+ /**
+ * Verifies that partition-level column statistics are not deleted when the
table has the
+ * exclude property set to {@code "true"}.
+ */
+ @Test
+ public void testExcludedTablePartitionStatsAreNotDeleted() throws Exception {
+ String dbName = "stats_db5";
+ String tableName = "tbl5";
+ String partVal = "p1";
+ createDbAndPartitionedTable(dbName, tableName, true);
+ createPartition(dbName, tableName, partVal);
+
+ String partName = "part_col=" + partVal;
+ writePartitionLevelColStats(dbName, tableName, partName, "c1");
+ assertHasPartitionColStats(dbName, tableName, partName, "c1");
+ makeAllPartitionColStatsOlderThanRetention(dbName, tableName);
+
+ runStatisticsManagementTask(conf);
+
+ // Stats must still be present because the table is marked as excluded.
+ assertHasPartitionColStats(dbName, tableName, partName, "c1");
+ }
+
+ /**
+ * Verifies that fresh table-level column statistics whose {@code
lastAnalyzed} is within the
+ * retention period are not deleted when the task runs.
+ */
+ @Test
+ public void testFreshTableColStatsAreNotDeleted() throws Exception {
+ String dbName = "stats_db6";
+ String tableName = "tbl6";
+ createDbAndTable(dbName, tableName, false);
+
+ // Write stats but do NOT backdate lastAnalyzed — they are within the
retention period.
+ writeTableLevelColStats(dbName, tableName, "c1");
+ assertHasTableColStats(dbName, tableName, "c1");
+
+ runStatisticsManagementTask(conf);
+
+ // Stats are fresh and must not be deleted.
+ assertHasTableColStats(dbName, tableName, "c1");
+ }
+
+ /**
+ * Verifies that fresh partition-level column statistics whose {@code
lastAnalyzed} is within the
+ * retention period are not deleted when the task runs.
+ */
+ @Test
+ public void testFreshPartitionColStatsAreNotDeleted() throws Exception {
+ String dbName = "stats_db7";
+ String tableName = "tbl7";
+ String partVal = "p1";
+ createDbAndPartitionedTable(dbName, tableName, false);
+ createPartition(dbName, tableName, partVal);
+
+ // Write stats but do NOT backdate lastAnalyzed — they are within the
retention period.
+ String partName = "part_col=" + partVal;
+ writePartitionLevelColStats(dbName, tableName, partName, "c1");
+ assertHasPartitionColStats(dbName, tableName, partName, "c1");
+
+ runStatisticsManagementTask(conf);
+
+ // Stats are fresh and must not be deleted.
+ assertHasPartitionColStats(dbName, tableName, partName, "c1");
+ }
+
+ /**
+ * Creates a database (unless it is the default database) and a simple
two-column test table.
+ *
+ * @param dbName name of the database to create
+ * @param tableName name of the table to create
+ * @param exclude if {@code true}, sets the auto-deletion exclude property
on the table
+ */
+ private void createDbAndTable(String dbName, String tableName, boolean
exclude) throws Exception {
+ Database db;
+ if (!DEFAULT_DATABASE_NAME.equals(dbName)) {
+ db = new DatabaseBuilder()
+ .setName(dbName)
+ .setCatalogName(DEFAULT_CATALOG_NAME)
+ .create(client, conf);
+ } else {
+ db = client.getDatabase(DEFAULT_CATALOG_NAME, DEFAULT_DATABASE_NAME);
+ }
+
+ TableBuilder tb = new TableBuilder()
+ .inDb(db)
+ .setTableName(tableName)
+ .addCol("c1", "double")
+ .addCol("c2", "string")
+ .setInputFormat("org.apache.hadoop.hive.ql.io.orc.OrcInputFormat")
+ .setOutputFormat("org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat");
+
+ Table t = tb.build(conf);
+ if (exclude) {
+ t.getParameters().put(
+
StatisticsManagementTask.STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY, "true");
+ }
+ client.createTable(t);
+ client.flushCache();
+ }
+
+ /**
+ * Creates a database and a partitioned test table with one partition key
{@code part_col}.
+ *
+ * @param dbName name of the database to create
+ * @param tableName name of the table to create
+ * @param exclude if {@code true}, sets the auto-deletion exclude property
on the table
+ */
+ private void createDbAndPartitionedTable(String dbName, String tableName,
+ boolean exclude) throws Exception {
+ Database db = new DatabaseBuilder()
+ .setName(dbName)
+ .setCatalogName(DEFAULT_CATALOG_NAME)
+ .create(client, conf);
+
+ TableBuilder tb = new TableBuilder()
+ .inDb(db)
+ .setTableName(tableName)
+ .addCol("c1", "double")
+ .addCol("c2", "string")
+ .addPartCol("part_col", "string")
+ .setInputFormat("org.apache.hadoop.hive.ql.io.orc.OrcInputFormat")
+ .setOutputFormat("org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat");
+
+ Table t = tb.build(conf);
+ if (exclude) {
+ t.getParameters().put(
+
StatisticsManagementTask.STATISTICS_AUTO_DELETION_EXCLUDE_TBLPROPERTY, "true");
+ }
+ client.createTable(t);
+ client.flushCache();
+ }
+
+ /**
+ * Adds a single partition with the given value for {@code part_col} to the
specified table.
+ *
+ * @param dbName database name
+ * @param tableName table name
+ * @param partVal value for the {@code part_col} partition key
+ */
+ private void createPartition(String dbName, String tableName, String
partVal) throws Exception {
+ Table t = client.getTable(dbName, tableName);
+ Partition partition = new PartitionBuilder()
+ .inTable(t)
+ .addValue(partVal)
+ .build(conf);
+ client.add_partition(partition);
+ }
+
+ /**
+ * Writes minimal table-level column statistics for the given column via the
metastore client.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param col column name
+ */
+ private void writeTableLevelColStats(String db, String tbl, String col)
throws TException {
+ ColumnStatisticsObj obj = buildDoubleColStatsObj(col);
+
+ ColumnStatisticsDesc desc = new ColumnStatisticsDesc(true, db, tbl);
+ desc.setCatName(DEFAULT_CATALOG_NAME);
+
+ ColumnStatistics cs = new ColumnStatistics();
+ cs.setStatsDesc(desc);
+ cs.addToStatsObj(obj);
+
+ client.updateTableColumnStatistics(cs);
+ }
+
+ /**
+ * Writes minimal partition-level column statistics for the given column via
the metastore client.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param partName partition name (e.g. {@code "part_col=p1"})
+ * @param col column name
+ */
+ private void writePartitionLevelColStats(String db, String tbl, String
partName,
+ String col) throws TException {
+ ColumnStatisticsObj obj = buildDoubleColStatsObj(col);
+
+ ColumnStatisticsDesc desc = new ColumnStatisticsDesc(false, db, tbl);
+ desc.setCatName(DEFAULT_CATALOG_NAME);
+ desc.setPartName(partName);
+
+ ColumnStatistics cs = new ColumnStatistics();
+ cs.setStatsDesc(desc);
+ cs.addToStatsObj(obj);
+
+ client.updatePartitionColumnStatistics(cs);
+ }
+
+ /**
+ * Builds a minimal {@link ColumnStatisticsObj} for a double-typed column.
+ *
+ * @param col column name
+ * @return a populated {@link ColumnStatisticsObj}
+ */
+ private ColumnStatisticsObj buildDoubleColStatsObj(String col) {
+ DoubleColumnStatsData doubleData = new DoubleColumnStatsData();
+ doubleData.setNumNulls(0);
+ doubleData.setNumDVs(1);
+ doubleData.setLowValue(1.0);
+ doubleData.setHighValue(1.0);
+
+ ColumnStatisticsData data = new ColumnStatisticsData();
+ data.setDoubleStats(doubleData);
+
+ ColumnStatisticsObj obj = new ColumnStatisticsObj();
+ obj.setColName(col);
+ obj.setColType("double");
+ obj.setStatsData(data);
+ return obj;
+ }
+
+ /**
+ * Asserts that at least one column statistics object exists for the
specified column.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param col column name
+ */
+ private void assertHasTableColStats(String db, String tbl, String col)
throws TException {
+ List<ColumnStatisticsObj> objs =
+ client.getTableColumnStatistics(db, tbl,
Collections.singletonList(col), "hive");
+ assertTrue("Expected stats for " + db + "." + tbl + "." + col,
+ objs != null && !objs.isEmpty());
+ }
+
+ /**
+ * Asserts that no column statistics exist for the specified column.
+ * A {@link NoSuchObjectException} from the server is also treated as an
acceptable absence signal.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param col column name
+ */
+ private void assertNoTableColStats(String db, String tbl, String col) throws
TException {
+ try {
+ List<ColumnStatisticsObj> objs =
+ client.getTableColumnStatistics(db, tbl,
Collections.singletonList(col), "hive");
+ assertTrue("Expected no stats for " + db + "." + tbl + "." + col,
+ objs == null || objs.isEmpty());
+ } catch (NoSuchObjectException e) {
+ // Acceptable: server may throw if stats are absent depending on
implementation.
+ }
+ }
+
+ /**
+ * Asserts that at least one partition-level column statistics object exists
for the specified
+ * column and partition.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param partName partition name
+ * @param col column name
+ */
+ private void assertHasPartitionColStats(String db, String tbl, String
partName,
+ String col) throws TException {
+ Map<String, List<ColumnStatisticsObj>> statsMap =
client.getPartitionColumnStatistics(
+ db, tbl, Collections.singletonList(partName),
Collections.singletonList(col), "hive");
+ List<ColumnStatisticsObj> objs = statsMap == null ? null :
statsMap.get(partName);
+ assertTrue("Expected partition stats for " + db + "." + tbl + "/" +
partName + "." + col,
+ objs != null && !objs.isEmpty());
+ }
+
+ /**
+ * Asserts that no partition-level column statistics exist for the specified
column and partition.
+ * A {@link NoSuchObjectException} from the server is also treated as an
acceptable absence signal.
+ *
+ * @param db database name
+ * @param tbl table name
+ * @param partName partition name
+ * @param col column name
+ */
+ private void assertNoPartitionColStats(String db, String tbl, String
partName,
+ String col) throws TException {
+ try {
+ Map<String, List<ColumnStatisticsObj>> statsMap =
client.getPartitionColumnStatistics(
+ db, tbl, Collections.singletonList(partName),
Collections.singletonList(col), "hive");
+ List<ColumnStatisticsObj> objs = statsMap == null ? null :
statsMap.get(partName);
+ assertTrue("Expected no partition stats for " + db + "." + tbl + "/" +
partName + "." + col,
+ objs == null || objs.isEmpty());
+ } catch (NoSuchObjectException e) {
+ // Acceptable: server may throw if stats are absent depending on
implementation.
+ }
+ }
+
+ /**
+ * Backdates the {@code lastAnalyzed} field of all {@code
MTableColumnStatistics} rows for the
+ * given table to 400 days ago, making them appear expired relative to any
reasonable retention
+ * period. Uses a fresh {@link ObjectStore} instance to bypass the proxy
wrapper returned by
+ * {@code HMSHandler.getMSForConf()}.
+ *
+ * @param db database name
+ * @param tbl table name
+ */
+ private void makeAllTableColStatsOlderThanRetention(String db, String tbl)
throws Exception {
+ ObjectStore os = new ObjectStore();
+ os.setConf(conf);
+
+ long oldSeconds = (System.currentTimeMillis() -
TimeUnit.DAYS.toMillis(400)) / 1000;
+ PersistenceManager pm = os.getPersistenceManager();
+ try (Query q = pm.newQuery(MTableColumnStatistics.class)) {
+ q.setFilter("table.tableName == t && table.database.name == d"
+ + " && table.database.catalogName == c");
+ q.declareParameters("java.lang.String t, java.lang.String d,
java.lang.String c");
+ @SuppressWarnings("unchecked")
+ List<MTableColumnStatistics> rows =
+ (List<MTableColumnStatistics>) q.execute(tbl, db,
DEFAULT_CATALOG_NAME);
+ for (MTableColumnStatistics r : rows) {
+ r.setLastAnalyzed(oldSeconds);
+ }
+ pm.flush();
+ } finally {
+ pm.close();
+ }
+ }
+
+ /**
+ * Backdates the {@code lastAnalyzed} field of all {@code
MPartitionColumnStatistics} rows for
+ * the given table to 400 days ago. Uses a fresh {@link ObjectStore}
instance directly.
+ *
+ * @param db database name
+ * @param tbl table name
+ */
+ private void makeAllPartitionColStatsOlderThanRetention(String db, String
tbl) throws Exception {
+ ObjectStore os = new ObjectStore();
+ os.setConf(conf);
+
+ long oldSeconds = (System.currentTimeMillis() -
TimeUnit.DAYS.toMillis(400)) / 1000;
+ PersistenceManager pm = os.getPersistenceManager();
+ try (Query q = pm.newQuery(MPartitionColumnStatistics.class)) {
+ q.setFilter("partition.table.tableName == t &&
partition.table.database.name == d"
+ + " && partition.table.database.catalogName == c");
+ q.declareParameters("java.lang.String t, java.lang.String d,
java.lang.String c");
+ @SuppressWarnings("unchecked")
+ List<MPartitionColumnStatistics> rows =
+ (List<MPartitionColumnStatistics>) q.execute(tbl, db,
DEFAULT_CATALOG_NAME);
+ for (MPartitionColumnStatistics r : rows) {
+ r.setLastAnalyzed(oldSeconds);
+ }
+ pm.flush();
+ } finally {
+ pm.close();
+ }
+ }
+
+ /**
+ * Instantiates and runs a {@link StatisticsManagementTask} with the given
configuration.
+ *
+ * @param configuration the HMS configuration to pass to the task
+ */
+ private void runStatisticsManagementTask(Configuration configuration) {
+ StatisticsManagementTask task = new StatisticsManagementTask();
+ task.setConf(configuration);
+ task.run();
+ }
+}