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 fc0db4e3fd1 HIVE-30100: Post-run integrity checks for metatool column 
de-duplication (#6865)
fc0db4e3fd1 is described below

commit fc0db4e3fd1ea4734c0a37f1d3e0a00f33db7b2c
Author: dengzh <[email protected]>
AuthorDate: Fri Oct 16 08:32:27 2026 +0800

    HIVE-30100: Post-run integrity checks for metatool column de-duplication 
(#6865)
---
 .../hive/metastore/tools/ColumnDeduplicator.java   | 217 +++++++++++++++------
 .../hive/metastore/tools/MetaToolObjectStore.java  |   9 +
 .../tools/metatool/HiveMetaToolCommandLine.java    |   4 +-
 .../tools/metatool/MetaToolTaskDedupColumns.java   |   3 +
 .../metastore/TestHMSColumnDescriptorReuse.java    | 107 ++++++++++
 5 files changed, 277 insertions(+), 63 deletions(-)

diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/ColumnDeduplicator.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/ColumnDeduplicator.java
index b4854c80b3b..16cb5f29136 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/ColumnDeduplicator.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/ColumnDeduplicator.java
@@ -20,18 +20,19 @@
 package org.apache.hadoop.hive.metastore.tools;
 
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.hadoop.hive.metastore.Batchable;
 import org.apache.hadoop.hive.metastore.Deadline;
 import org.apache.hadoop.hive.metastore.RawStore;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
 import org.apache.hadoop.hive.metastore.api.FieldSchema;
 import org.apache.hadoop.hive.metastore.api.MetaException;
+import org.apache.hadoop.hive.metastore.directsql.MetastoreDirectSqlUtils;
 import org.apache.hadoop.hive.metastore.metastore.RawStoreBundle;
 import org.apache.hadoop.hive.metastore.model.MColumnDescriptor;
 import org.apache.hadoop.hive.metastore.model.MConstraint;
 import org.apache.hadoop.hive.metastore.model.MPartition;
 import org.apache.hadoop.hive.metastore.model.MStorageDescriptor;
 import org.apache.hadoop.hive.metastore.model.MTable;
-
-import javax.jdo.JDOHelper;
 import javax.jdo.PersistenceManager;
 import javax.jdo.Query;
 import java.util.ArrayList;
@@ -41,6 +42,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 import static 
org.apache.hadoop.hive.metastore.ObjectStore.appendPatternCondition;
@@ -53,14 +55,14 @@
  * Identical column schemas within a table are merged so that partitions share
  * the same CD, reducing metadata bloat that can accumulate during replication.
  */
-final class ColumnDeduplicator {
+public final class ColumnDeduplicator {
   private final RawStore store;
   private final PersistenceManager pm;
   private final AtomicReference<String> progress;
   private final boolean isDryRun;
   private final boolean isVerbose;
 
-  ColumnDeduplicator(RawStoreBundle bundle, AtomicReference<String> progress,
+  public ColumnDeduplicator(RawStoreBundle bundle, AtomicReference<String> 
progress,
       boolean isDryRun, boolean isVerbose) {
     this.store = bundle.getBaseStore();
     this.pm = bundle.getPersistentManager();
@@ -77,10 +79,11 @@ MetaToolObjectStore.DedupColumnsResult run(String 
catalogFilter, String dbFilter
     for (int i = 0; i < tables.size() && result.getException() == null; i++) {
       boolean committed = false;
       TableInfo table = tables.get(i);
+      PostCommitValidation validation = null;
       store.openTransaction();
       try {
         Deadline.checkTimeout();
-        deduplicateTable(table, result);
+        validation = deduplicateTable(table, result);
         committed = store.commitTransaction();
       } catch (Exception ex) {
         result.catchException(ex);
@@ -93,6 +96,16 @@ MetaToolObjectStore.DedupColumnsResult run(String 
catalogFilter, String dbFilter
           }
         }
       }
+
+      try {
+        if (committed && validation != null) {
+          validateTableAfterDedup(table, validation.schemaSnapshots());
+          result.incrementTablesValidated();
+        }
+      } catch (Exception e) {
+        result.catchException(new IllegalStateException("Failed to validate 
the column deduplication for this table: " + table, e));
+      }
+
       if (progress != null) {
         progress.set(String.format(
             "Finished %d tables in %d total tables, time taken: %d ms, columns 
updated: %d, removed: %d",
@@ -106,24 +119,23 @@ MetaToolObjectStore.DedupColumnsResult run(String 
catalogFilter, String dbFilter
     return result;
   }
 
-  private void deduplicateTable(TableInfo table, 
MetaToolObjectStore.DedupColumnsResult result) {
+  private PostCommitValidation deduplicateTable(TableInfo table, 
MetaToolObjectStore.DedupColumnsResult result)
+      throws MetaException {
     List<PartitionSdInfo> partitionSds = 
loadPartitionStorageDescriptors(table.tableId);
     if (partitionSds.isEmpty()) {
-      return;
+      return null;
     }
 
     Set<Long> cdIds = partitionSds.stream().map(p -> 
p.cdId).collect(Collectors.toSet());
     cdIds.add(table.tableCdId);
-
-    Map<Long, List<FieldSchema>> cdColumns = loadColumnSchemas(cdIds);
-    Map<List<FieldSchema>, List<Long>> groups = groupByColumnSchema(cdColumns);
+    Map<List<FieldSchema>, Set<Long>> groups = groupByColumnSchema(cdIds);
 
     Map<Long, Long> cdRemap = new HashMap<>();
-    for (List<Long> group : groups.values()) {
+    for (Set<Long> group : groups.values()) {
       if (group.size() <= 1) {
         continue;
       }
-      long canonicalCdId = pickCanonicalCdId(new HashSet<>(group), 
table.tableCdId, partitionSds);
+      long canonicalCdId = pickCanonicalCdId(group, table.tableCdId, 
partitionSds);
       for (long cdId : group) {
         if (cdId != canonicalCdId) {
           cdRemap.put(cdId, canonicalCdId);
@@ -131,32 +143,29 @@ private void deduplicateTable(TableInfo table, 
MetaToolObjectStore.DedupColumnsR
       }
     }
 
-    if (cdRemap.isEmpty()) {
-      return;
-    }
-
     List<Map.Entry<PartitionSdInfo, Long>> partSdUpdates = 
buildPartitionUpdates(partitionSds, cdRemap);
     if (partSdUpdates.isEmpty()) {
-      return;
+      return null;
     }
-
     result.incrementTablesWithDuplicates();
     for (Map.Entry<PartitionSdInfo, Long> update : partSdUpdates) {
       result.incrementStorageDescriptorsUpdated();
       if (isVerbose) {
         PartitionSdInfo partSd = update.getKey();
         long newCdId = update.getValue();
-        result.addDetail(String.format("table %s.%s.%s: SD %s CD %d -> %d",
-            table.catalogName, table.dbName, table.tableName,
-            JDOHelper.getObjectId(partSd.sd), partSd.cdId, newCdId));
+        result.addDetail(String.format("table %s.%s.%s: partition %d CD %d -> 
%d",
+            table.catalogName, table.dbName, table.tableName, partSd.partId, 
partSd.cdId, newCdId));
       }
     }
-    if (!isDryRun) {
-      applyTableChanges(partSdUpdates, result);
+    if (isDryRun) {
+      return null;
     }
+    List<PartitionSchemaSnapshot> schemaSnapshots = 
capturePartitionSchemas(partSdUpdates);
+    applySchemaChanges(partSdUpdates, result);
+    return new PostCommitValidation(schemaSnapshots);
   }
 
-  private void applyTableChanges(List<Map.Entry<PartitionSdInfo, Long>> 
partSdUpdates,
+  private void applySchemaChanges(List<Map.Entry<PartitionSdInfo, Long>> 
partSdUpdates,
       MetaToolObjectStore.DedupColumnsResult result) {
     Set<Long> replacedCdIds = new HashSet<>();
     Map<Long, MColumnDescriptor> newCDs = new HashMap<>();
@@ -174,6 +183,9 @@ private void 
applyTableChanges(List<Map.Entry<PartitionSdInfo, Long>> partSdUpda
   private List<Map.Entry<PartitionSdInfo, Long>> buildPartitionUpdates(
       List<PartitionSdInfo> partitionSds, Map<Long, Long> cdRemap) {
     List<Map.Entry<PartitionSdInfo, Long>> updates = new ArrayList<>();
+    if (cdRemap.isEmpty()) {
+      return updates;
+    }
     for (PartitionSdInfo partSd : partitionSds) {
       Long newCdId = cdRemap.get(partSd.cdId);
       if (newCdId != null && !newCdId.equals(partSd.cdId)) {
@@ -245,20 +257,24 @@ private List<TableInfo> findPartitionedTables(String 
catalogFilter, String dbFil
     return tables;
   }
 
-  private List<PartitionSdInfo> loadPartitionStorageDescriptors(long tableId) {
+  private List<PartitionSdInfo> loadPartitionStorageDescriptors(long tableId) 
throws MetaException {
     Query query = pm.newQuery(MPartition.class, "table.id == tblId");
     query.declareParameters("long tblId");
-    query.setResult("sd");
+    query.setResult("this.id, this.sd");
     List<PartitionSdInfo> partitionSds = new ArrayList<>();
     try {
-      List<MStorageDescriptor> sds = (List<MStorageDescriptor>) 
query.execute(tableId);
-      if (sds == null) {
+      List<Object[]> results = (List<Object[]>) query.execute(tableId);
+      if (results == null) {
         return partitionSds;
       }
-      pm.retrieveAll(sds);
-      for (MStorageDescriptor sd : sds) {
-        pm.retrieve(sd.getCD());
-        partitionSds.add(new PartitionSdInfo(sd, sd.getCD().getId()));
+      for (Object[] row : results) {
+        long partId = MetastoreDirectSqlUtils.extractSqlLong(row[0]);
+        MStorageDescriptor sd = (MStorageDescriptor) row[1];
+        if (sd == null) {
+          continue;
+        }
+        pm.retrieve(sd);
+        partitionSds.add(new PartitionSdInfo(partId, sd, sd.getCD().getId()));
       }
     } finally {
       query.closeAll();
@@ -266,24 +282,110 @@ private List<PartitionSdInfo> 
loadPartitionStorageDescriptors(long tableId) {
     return partitionSds;
   }
 
-  private Map<Long, List<FieldSchema>> loadColumnSchemas(Set<Long> cdIds) {
-    Map<Long, List<FieldSchema>> result = new HashMap<>();
+  private List<PartitionSchemaSnapshot> 
capturePartitionSchemas(List<Map.Entry<PartitionSdInfo, Long>> updates) {
+    List<PartitionSchemaSnapshot> snapshots = new ArrayList<>(updates.size());
+    for (Map.Entry<PartitionSdInfo, Long> update: updates) {
+      PartitionSdInfo psi = update.getKey();
+      snapshots.add(new PartitionSchemaSnapshot(
+          psi.partId,
+          update.getValue(),
+          convertToFieldSchemas(psi.sd.getCD().getCols())));
+    }
+    return snapshots;
+  }
+
+  public void validateTableAfterDedup(TableInfo table, 
List<PartitionSchemaSnapshot> before)
+      throws MetaException {
+    List<PartitionSchemaSnapshot> partitions =
+        loadPartitionCdRefs(before.stream().map(b -> b.partitionId).toList());
+    if (partitions.size() != before.size()) {
+      throw new MetaException(String.format(
+          "Post-commit validation failed for %s: expected %d partitions, found 
%d",
+          table, before.size(), partitions.size()));
+    }
+    Map<Long, PartitionSchemaSnapshot> beforeByPartitionId = new HashMap<>();
+    for (PartitionSchemaSnapshot snapshot : before) {
+      beforeByPartitionId.put(snapshot.partitionId(), snapshot);
+    }
+    for (PartitionSchemaSnapshot partition : partitions) {
+      PartitionSchemaSnapshot snapshot = 
beforeByPartitionId.get(partition.partitionId());
+      if (snapshot == null) {
+        throw new MetaException(String.format(
+            "Post-commit validation failed for %s: unexpected partition id %d",
+            table, partition.partitionId()));
+      }
+      if (partition.cdId() != snapshot.cdId()) {
+        throw new MetaException(String.format(
+            "Post-commit validation failed for %s: partition %d still 
references CD %d, expected canonical CD %d",
+            table, partition.partitionId(), partition.cdId(), 
snapshot.cdId()));
+      }
+      List<FieldSchema> expected = snapshot.columns();
+      List<FieldSchema> actual = partition.columns();
+      if (!Objects.equals(expected, actual)) {
+        throw new MetaException(String.format(
+            "Post-commit validation failed for %s: column schema changed for 
partition %d",
+            table, partition.partitionId()));
+      }
+    }
+  }
+
+  private List<PartitionSchemaSnapshot> loadPartitionCdRefs(List<Long> 
partIds) throws MetaException {
+    if (partIds.isEmpty()) {
+      return new ArrayList<>();
+    }
+    int batchSize = MetastoreConf.getIntVar(store.getConf(),
+        MetastoreConf.ConfVars.RAWSTORE_PARTITION_BATCH_SIZE);
+    Batchable<Long, PartitionSchemaSnapshot> batch = new Batchable<>() {
+      @Override
+      public List<PartitionSchemaSnapshot> run(List<Long> batchPartIds) throws 
Exception {
+        Query query = pm.newQuery(MPartition.class, "partIds.contains(id)");
+        query.declareParameters("java.util.Collection partIds");
+        query.setResult("this.id, this.sd");
+        addQueryAfterUse(query);
+        List<PartitionSchemaSnapshot> partitions = new ArrayList<>();
+        boolean success = false;
+        store.openTransaction();
+        try {
+          List<Object[]> results = (List<Object[]>) 
query.execute(batchPartIds);
+          if (results != null) {
+            for (Object[] row : results) {
+              long partId = MetastoreDirectSqlUtils.extractSqlLong(row[0]);
+              MStorageDescriptor sd = (MStorageDescriptor) row[1];
+              if (sd == null) {
+                continue;
+              }
+              pm.retrieve(sd.getCD());
+              partitions.add(new PartitionSchemaSnapshot(partId, 
sd.getCD().getId(),
+                  convertToFieldSchemas(sd.getCD().getCols())));
+            }
+          }
+          success = store.commitTransaction();
+        } finally {
+          if (!success) {
+            store.rollbackTransaction();
+          }
+        }
+        return partitions;
+      }
+    };
+    try {
+      return Batchable.runBatched(batchSize, partIds, batch);
+    } finally {
+      batch.closeAllQueries();
+    }
+  }
+
+  private Map<List<FieldSchema>, Set<Long>> groupByColumnSchema(Set<Long> 
cdIds) {
+    Map<List<FieldSchema>, Set<Long>> groups = new HashMap<>();
     for (Long cdId : cdIds) {
       MColumnDescriptor cd = pm.getObjectById(MColumnDescriptor.class, cdId);
       if (cd != null) {
         pm.retrieve(cd);
-        result.put(cdId, convertToFieldSchemas(cd.getCols()));
+        List<FieldSchema> schemas = convertToFieldSchemas(cd.getCols());
+        groups.computeIfAbsent(schemas, k -> new HashSet<>()).add(cdId);
+        schemas = null;
       }
     }
-    return result;
-  }
-
-  private static Map<List<FieldSchema>, List<Long>> 
groupByColumnSchema(Map<Long, List<FieldSchema>> cdColumns) {
-    Map<List<FieldSchema>, List<Long>> groups = new HashMap<>();
-    for (Map.Entry<Long, List<FieldSchema>> entry : cdColumns.entrySet()) {
-      groups.computeIfAbsent(entry.getValue(), ignored -> new ArrayList<>())
-          .add(entry.getKey());
-    }
     return groups;
   }
 
@@ -316,28 +418,19 @@ private void removeConstraintsForCd(MColumnDescriptor cd) 
{
     }
   }
 
-  private static final class TableInfo {
-    private final long tableId;
-    private final long tableCdId;
-    private final String catalogName;
-    private final String dbName;
-    private final String tableName;
-
-    private TableInfo(long tableId, long tableCdId, String catalogName, String 
dbName, String tableName) {
-      this.tableId = tableId;
-      this.tableCdId = tableCdId;
-      this.catalogName = catalogName;
-      this.dbName = dbName;
-      this.tableName = tableName;
-    }
-
+  public record TableInfo(long tableId, long tableCdId, String catalogName, 
String dbName, String tableName) {
     @Override
     public String toString() {
-      return catalogName + "." + dbName + "." + tableName;
-    }
+        return catalogName + "." + dbName + "." + tableName;
+      }
   }
 
-  private record PartitionSdInfo(MStorageDescriptor sd, long cdId) {
+  private record PartitionSdInfo(long partId, MStorageDescriptor sd, long 
cdId) {
+  }
+
+  public record PartitionSchemaSnapshot(long partitionId, long cdId, 
List<FieldSchema> columns) {
+  }
 
+  private record PostCommitValidation(List<PartitionSchemaSnapshot> 
schemaSnapshots) {
   }
 }
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/MetaToolObjectStore.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/MetaToolObjectStore.java
index 67a97243bd5..5c5a6124270 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/MetaToolObjectStore.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/MetaToolObjectStore.java
@@ -975,6 +975,7 @@ public static final class DedupColumnsResult {
     private int tablesWithDuplicates;
     private int storageDescriptorsUpdated;
     private int columnDescriptorsRemoved;
+    private int tablesValidated;
     private final List<String> details = new ArrayList<>();
     private Exception exception;
     DedupColumnsResult(int tablesScanned) {
@@ -1008,6 +1009,14 @@ void addColumnDescriptorsRemoved(int count) {
       this.columnDescriptorsRemoved += count;
     }
 
+    public int getTablesValidated() {
+      return tablesValidated;
+    }
+
+    void incrementTablesValidated() {
+      this.tablesValidated++;
+    }
+
     public List<String> getDetails() {
       return details;
     }
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/HiveMetaToolCommandLine.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/HiveMetaToolCommandLine.java
index a1c9db7a327..5bec7e3bd97 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/HiveMetaToolCommandLine.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/HiveMetaToolCommandLine.java
@@ -93,7 +93,9 @@ class HiveMetaToolCommandLine {
       .hasOptionalArgs(3)
       .withDescription("De-duplicate column descriptors for partitioned tables 
to reduce metadata bloat. "
           + "Optional catalog, database, and table filters support * 
wildcards. Use -dryRun to preview changes, "
-          + "-verbose to print per-partition details, and -timeout to set a 
timeout in seconds.")
+          + "-verbose to print per-partition details, and -timeout to set a 
timeout in seconds. "
+          + "Live runs validate committed state (partition column schemas 
unchanged, canonical CD IDs applied) "
+          + "and print a post-commit validation summary.")
       .create("dedupColumns");
 
   private static final Option VERBOSE = OptionBuilder
diff --git 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/MetaToolTaskDedupColumns.java
 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/MetaToolTaskDedupColumns.java
index 06994e5d493..f86c7b9480c 100644
--- 
a/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/MetaToolTaskDedupColumns.java
+++ 
b/standalone-metastore/metastore-server/src/main/java/org/apache/hadoop/hive/metastore/tools/metatool/MetaToolTaskDedupColumns.java
@@ -110,6 +110,9 @@ private void 
printSummary(MetaToolObjectStore.DedupColumnsResult result, boolean
         + result.getStorageDescriptorsUpdated());
     if (!isDryRun) {
       System.out.println("Column descriptors removed: " + 
result.getColumnDescriptorsRemoved());
+      if (result.getTablesValidated() > 0) {
+        System.out.println("Post-commit validation passed for " + 
result.getTablesValidated() + " table(s).");
+      }
     }
     if (isVerbose) {
       for (String detail : result.getDetails()) {
diff --git 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestHMSColumnDescriptorReuse.java
 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestHMSColumnDescriptorReuse.java
index 483c15fccba..0f63bda5f80 100644
--- 
a/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestHMSColumnDescriptorReuse.java
+++ 
b/standalone-metastore/metastore-server/src/test/java/org/apache/hadoop/hive/metastore/TestHMSColumnDescriptorReuse.java
@@ -34,6 +34,7 @@
 import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
 import org.apache.hadoop.hive.metastore.dbinstall.rules.DatabaseRule;
 import org.apache.hadoop.hive.metastore.dbinstall.rules.Derby;
+import org.apache.hadoop.hive.metastore.tools.ColumnDeduplicator;
 import org.apache.hadoop.hive.metastore.tools.MetaToolObjectStore;
 import org.apache.hadoop.hive.metastore.utils.TestTxnDbUtil;
 import org.junit.After;
@@ -42,6 +43,7 @@
 import org.junit.experimental.categories.Category;
 
 import java.sql.Connection;
+import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.util.Arrays;
 import java.util.Collections;
@@ -53,10 +55,12 @@
 import static org.apache.hadoop.hive.metastore.Warehouse.DEFAULT_CATALOG_NAME;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 @Category(MetastoreUnitTest.class)
 public class TestHMSColumnDescriptorReuse {
+
   private ObjectStore objectStore = null;
   private Configuration conf;
 
@@ -196,6 +200,7 @@ public void testDeduplicateColumnDescriptorsTool() throws 
Exception {
       MetaToolObjectStore.DedupColumnsResult result = 
metaToolStore.dedupColumns(null, "default", "person", progress,
           false, false);
       assertEquals(0, result.getTablesWithDuplicates());
+      assertEquals(0, result.getTablesValidated());
 
       FieldSchema lname = new FieldSchema("lname", 
ColumnType.STRING_TYPE_NAME, "");
       Table tbl2 = newTable(Arrays.asList(id, fname, lname), 
Collections.singletonList(country));
@@ -209,6 +214,7 @@ public void testDeduplicateColumnDescriptorsTool() throws 
Exception {
 
       result = metaToolStore.dedupColumns(null, "default", "person", progress, 
false, false);
       assertTrue(result.getStorageDescriptorsUpdated() > 0);
+      assertEquals(1, result.getTablesValidated());
       assertEquals(2, countColumnDescriptors());
       assertNotNull(progress.get());
 
@@ -230,6 +236,107 @@ public void testDeduplicateColumnDescriptorsTool() throws 
Exception {
     }
   }
 
+  @Test
+  public void testValidateTableAfterDedupReloadsPartitionById() throws 
Exception {
+    MetastoreConf.setBoolVar(conf, 
MetastoreConf.ConfVars.PARTITION_REUSE_COLUMN_DESCRIPTORS, false);
+    createTableWithDuplicateColumnDescriptors();
+    long partId = lookupPartitionId("country=Germany");
+    List<FieldSchema> cols = Arrays.asList(
+        new FieldSchema("id", ColumnType.STRING_TYPE_NAME, ""),
+        new FieldSchema("fname", ColumnType.STRING_TYPE_NAME, ""));
+
+    validate(Collections.singletonList(new 
ColumnDeduplicator.PartitionSchemaSnapshot(
+        partId, lookupPartitionCdId(partId), cols)));
+  }
+
+  @Test
+  public void testValidateTableAfterDedupDetectsCdRemapFailure() throws 
Exception {
+    MetastoreConf.setBoolVar(conf, 
MetastoreConf.ConfVars.PARTITION_REUSE_COLUMN_DESCRIPTORS, false);
+    createTableWithDuplicateColumnDescriptors();
+    long partId = lookupPartitionId("country=Germany");
+    List<FieldSchema> cols = Arrays.asList(
+        new FieldSchema("id", ColumnType.STRING_TYPE_NAME, ""),
+        new FieldSchema("fname", ColumnType.STRING_TYPE_NAME, ""));
+
+    List<ColumnDeduplicator.PartitionSchemaSnapshot> snapshots = 
Collections.singletonList(
+        new ColumnDeduplicator.PartitionSchemaSnapshot(partId, -1L, cols));
+
+    MetaException ex = assertThrows(MetaException.class, () -> 
validate(snapshots));
+    assertTrue(ex.getMessage().contains("expected canonical CD"));
+  }
+
+  @Test
+  public void testValidateTableAfterDedupDetectsSchemaChange() throws 
Exception {
+    MetastoreConf.setBoolVar(conf, 
MetastoreConf.ConfVars.PARTITION_REUSE_COLUMN_DESCRIPTORS, false);
+    createTableWithDuplicateColumnDescriptors();
+    long partId = lookupPartitionId("country=Germany");
+
+    List<ColumnDeduplicator.PartitionSchemaSnapshot> snapshots = 
Collections.singletonList(
+        new ColumnDeduplicator.PartitionSchemaSnapshot(partId, 
lookupPartitionCdId(partId),
+            Collections.singletonList(new FieldSchema("unexpected", 
ColumnType.STRING_TYPE_NAME, ""))));
+
+    MetaException ex = assertThrows(MetaException.class, () -> 
validate(snapshots));
+    assertTrue(ex.getMessage().contains("column schema changed for partition " 
+ partId));
+  }
+
+  private int createTableWithDuplicateColumnDescriptors() throws Exception {
+    FieldSchema id = new FieldSchema("id", ColumnType.STRING_TYPE_NAME, "");
+    FieldSchema fname = new FieldSchema("fname", ColumnType.STRING_TYPE_NAME, 
"");
+    FieldSchema country = new FieldSchema("country", 
ColumnType.STRING_TYPE_NAME, "");
+
+    Table tbl1 = newTable(Arrays.asList(id, fname), 
Collections.singletonList(country));
+    objectStore.createTable(tbl1);
+    objectStore.addPartition(newPart(tbl1, "US"));
+    objectStore.addPartition(newPart(tbl1, "Greece"));
+
+    FieldSchema lname = new FieldSchema("lname", ColumnType.STRING_TYPE_NAME, 
"");
+    Table tbl2 = newTable(Arrays.asList(id, fname, lname), 
Collections.singletonList(country));
+    objectStore.alterTable(DEFAULT_CATALOG_NAME, tbl1.getDbName(), 
tbl1.getTableName(), tbl2, null);
+    objectStore.addPartition(newPart(tbl2, "Italy"));
+    objectStore.addPartition(newPart(tbl1, "Germany"));
+    objectStore.addPartition(newPart(tbl1, "Belgium"));
+    objectStore.addPartition(newPart(tbl2, "England"));
+
+    return countColumnDescriptors();
+  }
+
+  private long lookupPartitionId(String partName) throws Exception {
+    try (Connection c = TestTxnDbUtil.getConnection(conf);
+        PreparedStatement ps = c.prepareStatement(
+            "SELECT \"PART_ID\" FROM \"PARTITIONS\" WHERE \"PART_NAME\" = ?")) 
{
+      ps.setString(1, partName);
+      try (ResultSet rs = ps.executeQuery()) {
+        if (!rs.next()) {
+          throw new IllegalStateException("Partition not found: " + partName);
+        }
+        return rs.getLong(1);
+      }
+    }
+  }
+
+  private long lookupPartitionCdId(long partId) throws Exception {
+    try (Connection c = TestTxnDbUtil.getConnection(conf);
+        PreparedStatement ps = c.prepareStatement(
+            "SELECT s.\"CD_ID\" FROM \"PARTITIONS\" p JOIN \"SDS\" s ON 
p.\"SD_ID\" = s.\"SD_ID\" "
+                + "WHERE p.\"PART_ID\" = ?")) {
+      ps.setLong(1, partId);
+      try (ResultSet rs = ps.executeQuery()) {
+        if (!rs.next()) {
+          throw new IllegalStateException("Partition CD not found: " + partId);
+        }
+        return rs.getLong(1);
+      }
+    }
+  }
+
+  private void validate(List<ColumnDeduplicator.PartitionSchemaSnapshot> 
snapshots)
+      throws Exception {
+    ColumnDeduplicator deduplicator =
+        new ColumnDeduplicator(objectStore.createRawStoreBundle(), null, 
false, false);
+    ColumnDeduplicator.TableInfo tableInfo = new 
ColumnDeduplicator.TableInfo(0L, 0L, "hive", "default", "person");
+    deduplicator.validateTableAfterDedup(tableInfo, snapshots);
+  }
+
   private int countColumnDescriptors() {
     try(Connection c = TestTxnDbUtil.getConnection(conf)){
       try(ResultSet rs = c.prepareStatement("SELECT COUNT(*) FROM 
\"CDS\"").executeQuery()) {

Reply via email to