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

Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new d3893e27874 Parse validationTypesToSkip once and apply it consistently 
(#20019)
d3893e27874 is described below

commit d3893e278740913c7733dd67d65db4e0d5c2e46c
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Fri Oct 23 16:44:43 2026 -0700

    Parse validationTypesToSkip once and apply it consistently (#20019)
---
 .../pinot/client/admin/TableAdminClient.java       |   6 +-
 .../api/resources/PinotTableRestletResource.java   |  29 +-
 .../api/resources/TableConfigValidationUtils.java  |  51 ++-
 .../api/resources/TableConfigsRestletResource.java |  43 +--
 .../pinot/controller/util/TaskConfigUtils.java     |  24 +-
 .../api/TableConfigsRestletResourceTest.java       |   7 -
 .../pinot/controller/util/TaskConfigUtilsTest.java |  16 +
 .../segment/local/utils/TableConfigUtils.java      | 424 +++++++++++----------
 .../segment/local/utils/TableConfigUtilsTest.java  | 200 ++++++----
 9 files changed, 454 insertions(+), 346 deletions(-)

diff --git 
a/pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TableAdminClient.java
 
b/pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TableAdminClient.java
index da5adfe235f..7f64c2533ea 100644
--- 
a/pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TableAdminClient.java
+++ 
b/pinot-clients/pinot-java-client/src/main/java/org/apache/pinot/client/admin/TableAdminClient.java
@@ -878,7 +878,7 @@ public class TableAdminClient extends 
BaseServiceAdminClient {
   /// Creates TableConfigs (schema + table configs). Supports skipping 
validations and ignoring active tasks.
   ///
   /// @param tableConfigsJson TableConfigs JSON payload
-  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip (ALL|TASK|UPSERT)
+  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip
   /// @param ignoreActiveTasks Optional flag to ignore active tasks during 
creation
   public String createTableConfigs(String tableConfigsJson, @Nullable String 
validationTypesToSkip,
       @Nullable Boolean ignoreActiveTasks)
@@ -902,7 +902,7 @@ public class TableAdminClient extends 
BaseServiceAdminClient {
   ///
   /// @param tableName Raw table name
   /// @param tableConfigsJson Updated TableConfigs JSON payload
-  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip (ALL|TASK|UPSERT)
+  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip
   /// @param reload Whether to reload the table if schema change is backward 
compatible
   /// @param forceTableSchemaUpdate Whether to force schema update
   public String updateTableConfigs(String tableName, String tableConfigsJson, 
@Nullable String validationTypesToSkip,
@@ -937,7 +937,7 @@ public class TableAdminClient extends 
BaseServiceAdminClient {
   /// Validates a TableConfigs payload without applying it.
   ///
   /// @param tableConfigsJson TableConfigs JSON payload
-  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip (ALL|TASK|UPSERT)
+  /// @param validationTypesToSkip Optional comma-separated validation types 
to skip
   public String validateTableConfigs(String tableConfigsJson, @Nullable String 
validationTypesToSkip)
       throws PinotAdminException {
     return validateTableConfigs(tableConfigsJson, validationTypesToSkip, null);
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
index 3a3ad2c7a87..1046b2a17c8 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
@@ -120,6 +120,7 @@ import org.apache.pinot.core.auth.Authorize;
 import org.apache.pinot.core.auth.ManualAuthorization;
 import org.apache.pinot.core.auth.TargetType;
 import org.apache.pinot.segment.local.utils.TableConfigUtils;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableConfigValidatorRegistry;
 import org.apache.pinot.spi.config.table.TableStatsHumanReadable;
@@ -217,11 +218,12 @@ public class PinotTableRestletResource {
   @ApiOperation(value = "Adds a table", notes = "Adds a table")
   @ManualAuthorization // performed after parsing table configs
   public ConfigSuccessResponse addTable(String tableConfigStr,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: (ALL|TASK|UPSERT)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip,
       @DefaultValue("false") @QueryParam("ignoreActiveTasks") boolean 
ignoreActiveTasks,
       @Context HttpHeaders httpHeaders, @Context Request request)
       throws IOException {
+    EnumSet<ValidationType> skipTypes = 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip);
     // TODO introduce a table config ctor with json string.
     Pair<TableConfig, Map<String, Object>> 
tableConfigAndUnrecognizedProperties;
     TableConfig tableConfig;
@@ -251,7 +253,7 @@ public class PinotTableRestletResource {
       TableConfigTunerUtils.applyTunerConfigs(_pinotHelixResourceManager, 
tableConfig, schema, Map.of());
 
       TableConfigValidationUtils.validateTableConfig(
-          tableConfig, schema, typesToSkip, _pinotHelixResourceManager, 
_controllerConf, _pinotTaskManager);
+          tableConfig, schema, skipTypes, _pinotHelixResourceManager, 
_controllerConf, _pinotTaskManager);
     } catch (TableAlreadyExistsException e) {
       throw new ControllerApplicationException(LOGGER, e.getMessage(), 
Response.Status.CONFLICT, e);
     } catch (Exception e) {
@@ -761,13 +763,14 @@ public class PinotTableRestletResource {
   @ApiOperation(value = "Updates table config for a table", notes = "Updates 
table config for a table")
   public ConfigSuccessResponse updateTableConfig(
       @ApiParam(value = "Name of the table to update", required = true) 
@PathParam("tableName") String tableName,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: (ALL|TASK|UPSERT)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip,
       @ApiParam(value = "Force config changes")
       @QueryParam("force") @DefaultValue("false") boolean force,
       @Context HttpHeaders headers,
       String tableConfigString)
       throws Exception {
+    EnumSet<ValidationType> skipTypes = 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip);
     Pair<TableConfig, Map<String, Object>> 
tableConfigAndUnrecognizedProperties;
     TableConfig tableConfig;
     String tableNameWithType;
@@ -788,9 +791,8 @@ public class PinotTableRestletResource {
 
       schema = _pinotHelixResourceManager.getTableSchema(tableNameWithType);
       Preconditions.checkState(schema != null, "Failed to find schema for 
table: %s", tableNameWithType);
-      TableConfigValidationUtils.validateTableConfig(
-          tableConfig, schema, typesToSkip, _pinotHelixResourceManager, 
_controllerConf, _pinotTaskManager,
-          _pinotHelixResourceManager.getTableConfig(tableNameWithType));
+      TableConfigValidationUtils.validateTableConfig(tableConfig, schema, 
skipTypes, _pinotHelixResourceManager,
+          _controllerConf, _pinotTaskManager, 
_pinotHelixResourceManager.getTableConfig(tableNameWithType));
     } catch (Exception e) {
       String msg = String.format("Invalid table config: %s with error: %s", 
tableName, e.getMessage());
       throw new ControllerApplicationException(LOGGER, msg, 
Response.Status.BAD_REQUEST, e);
@@ -827,7 +829,7 @@ public class PinotTableRestletResource {
           + " This allows us to validate table config before apply.")
   @ManualAuthorization // performed after parsing TableConfig
   public ObjectNode checkTableConfig(String tableConfigStr,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: (ALL|TASK|UPSERT)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, 
@Context HttpHeaders httpHeaders,
       @Context Request request) {
     Pair<TableConfig, Map<String, Object>> 
tableConfigAndUnrecognizedProperties;
@@ -843,26 +845,27 @@ public class PinotTableRestletResource {
     tableConfig.setTableName(tableNameWithType);
 
     // validate permission
-    ResourceUtils.checkPermissionAndAccess(tableNameWithType, request, 
httpHeaders,
-        AccessType.READ, Actions.Table.VALIDATE_TABLE_CONFIGS, 
_accessControlFactory, LOGGER);
+    ResourceUtils.checkPermissionAndAccess(tableNameWithType, request, 
httpHeaders, AccessType.READ,
+        Actions.Table.VALIDATE_TABLE_CONFIGS, _accessControlFactory, LOGGER);
 
-    ObjectNode validationResponse = validateConfig(tableConfig, typesToSkip);
+    ObjectNode validationResponse =
+        validateConfig(tableConfig, 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip));
     validationResponse.set("unrecognizedProperties",
         
JsonUtils.objectToJsonNode(tableConfigAndUnrecognizedProperties.getRight()));
     return validationResponse;
   }
 
   @VisibleForTesting
-  ObjectNode validateConfig(TableConfig tableConfig, @Nullable String 
typesToSkip) {
+  ObjectNode validateConfig(TableConfig tableConfig, @Nullable 
EnumSet<ValidationType> skipTypes) {
     String tableNameWithType = tableConfig.getTableName();
     try {
       Schema schema = 
_pinotHelixResourceManager.getTableSchema(tableNameWithType);
       if (schema == null) {
         throw new SchemaNotFoundException("Failed to find schema for table: " 
+ tableNameWithType);
       }
-      TableConfigUtils.validate(tableConfig, schema, typesToSkip,
+      TableConfigUtils.validate(tableConfig, schema, skipTypes,
           _pinotHelixResourceManager.getTableConfig(tableNameWithType));
-      TaskConfigUtils.validateTaskConfigs(tableConfig, schema, 
_pinotTaskManager, typesToSkip);
+      TaskConfigUtils.validateTaskConfigs(tableConfig, schema, 
_pinotTaskManager, skipTypes);
       TableConfigValidatorRegistry.validate(tableConfig, schema);
       ObjectNode tableConfigValidateStr = JsonUtils.newObjectNode();
       if (tableConfig.getTableType() == TableType.OFFLINE) {
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
index 19692575c01..ef68c576a76 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigValidationUtils.java
@@ -20,19 +20,24 @@ package org.apache.pinot.controller.api.resources;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
+import javax.ws.rs.core.Response;
 import org.apache.helix.model.InstanceConfig;
 import org.apache.pinot.common.assignment.InstanceAssignmentConfigUtils;
 import org.apache.pinot.common.assignment.InstancePartitions;
 import org.apache.pinot.common.assignment.InstancePartitionsUtils;
 import org.apache.pinot.controller.ControllerConf;
+import 
org.apache.pinot.controller.api.exception.ControllerApplicationException;
 import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
 import 
org.apache.pinot.controller.helix.core.assignment.instance.InstanceAssignmentUtils;
 import org.apache.pinot.controller.helix.core.minion.PinotTaskManager;
 import org.apache.pinot.controller.util.TaskConfigUtils;
 import org.apache.pinot.segment.local.utils.TableConfigUtils;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.spi.config.ConfigUtils;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableConfigValidatorRegistry;
@@ -40,6 +45,8 @@ import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.config.table.assignment.InstancePartitionsType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 
 /// Utility class that encapsulates table config validation logic.
@@ -52,31 +59,55 @@ public final class TableConfigValidationUtils {
   private TableConfigValidationUtils() {
   }
 
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(TableConfigValidationUtils.class);
+
+  /// Description of the `validationTypesToSkip` request parameter parsed by 
[#parseValidationTypesToSkip].
+  public static final String VALIDATION_TYPES_TO_SKIP_DESCRIPTION =
+      "Comma separated list of validation type(s) to skip. Supported types: "
+          + 
"(ALL|TASK|UPSERT|DEDUP|INSTANCE_ASSIGNMENT|TENANT|MINION_INSTANCES). TENANT 
and MINION_INSTANCES apply "
+          + "only to validating and tuning table configs, as creating or 
updating a table always validates them";
+
+  /// Parses the `validationTypesToSkip` request parameter, rejecting an 
unknown validation type as a bad request.
+  /// Returns `null` when the parameter is not set or blank.
+  @Nullable
+  public static EnumSet<ValidationType> parseValidationTypesToSkip(@Nullable 
String typesToSkip) {
+    try {
+      return TableConfigUtils.parseTypesToSkipString(typesToSkip);
+    } catch (IllegalArgumentException e) {
+      throw new ControllerApplicationException(LOGGER,
+          String.format("Invalid validationTypesToSkip: %s, supported types: 
%s", typesToSkip,
+              Arrays.toString(ValidationType.values())), 
Response.Status.BAD_REQUEST, e);
+    }
+  }
+
   /// Validates a table config against the given schema and controller 
configuration.
   ///
   /// @param tableConfig   the table config to validate
   /// @param schema        the schema for the table (must not be null)
-  /// @param typesToSkip   comma-separated list of validation types to skip 
(ALL|TASK|UPSERT), or null
+  /// @param skipTypes     the validation types to skip, or null to skip none, 
see [ValidationType]
   /// @param resourceManager the Helix resource manager
   /// @param controllerConf  the controller configuration
   /// @param taskManager     the task manager, or null to skip task validation
-  public static void validateTableConfig(TableConfig tableConfig, Schema 
schema, @Nullable String typesToSkip,
-      PinotHelixResourceManager resourceManager, ControllerConf controllerConf,
-      @Nullable PinotTaskManager taskManager) {
-    validateTableConfig(tableConfig, schema, typesToSkip, resourceManager, 
controllerConf, taskManager, null);
+  public static void validateTableConfig(TableConfig tableConfig, Schema 
schema,
+      @Nullable EnumSet<ValidationType> skipTypes, PinotHelixResourceManager 
resourceManager,
+      ControllerConf controllerConf, @Nullable PinotTaskManager taskManager) {
+    validateTableConfig(tableConfig, schema, skipTypes, resourceManager, 
controllerConf, taskManager, null);
   }
 
-  public static void validateTableConfig(TableConfig tableConfig, Schema 
schema, @Nullable String typesToSkip,
-      PinotHelixResourceManager resourceManager, ControllerConf 
controllerConf, @Nullable PinotTaskManager taskManager,
+  public static void validateTableConfig(TableConfig tableConfig, Schema 
schema,
+      @Nullable EnumSet<ValidationType> skipTypes, PinotHelixResourceManager 
resourceManager,
+      ControllerConf controllerConf, @Nullable PinotTaskManager taskManager,
       @Nullable TableConfig existingTableConfig) {
     validateEnvironmentVariables(tableConfig);
-    TableConfigUtils.validate(tableConfig, schema, typesToSkip, 
existingTableConfig);
+    TableConfigUtils.validate(tableConfig, schema, skipTypes, 
existingTableConfig);
     TableConfigUtils.validateTableName(tableConfig);
     TableConfigUtils.ensureMinReplicas(tableConfig, 
controllerConf.getDefaultTableMinReplicas());
     TableConfigUtils.ensureStorageQuotaConstraints(tableConfig, 
controllerConf.getDimTableMaxSize());
     checkHybridTableConfig(resourceManager, tableConfig);
-    TaskConfigUtils.validateTaskConfigs(tableConfig, schema, taskManager, 
typesToSkip);
-    validateInstanceAssignment(resourceManager, tableConfig);
+    TaskConfigUtils.validateTaskConfigs(tableConfig, schema, taskManager, 
skipTypes);
+    if (!ValidationType.INSTANCE_ASSIGNMENT.isSkipped(skipTypes)) {
+      validateInstanceAssignment(resourceManager, tableConfig);
+    }
     resourceManager.validateTableTenantConfig(tableConfig);
     resourceManager.validateTableTaskMinionInstanceTagConfig(tableConfig);
     TableConfigValidatorRegistry.validate(tableConfig, schema);
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
index 579322765a1..825feaaed31 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
@@ -30,9 +30,9 @@ import io.swagger.annotations.SecurityDefinition;
 import io.swagger.annotations.SwaggerDefinition;
 import java.io.IOException;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 import javax.ws.rs.DELETE;
@@ -76,6 +76,7 @@ import org.apache.pinot.core.auth.ManualAuthorization;
 import org.apache.pinot.core.auth.TargetType;
 import org.apache.pinot.segment.local.utils.SchemaUtils;
 import org.apache.pinot.segment.local.utils.TableConfigUtils;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.spi.config.TableConfigs;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableConfigValidatorRegistry;
@@ -190,11 +191,12 @@ public class TableConfigsRestletResource {
   @ManualAuthorization // performed after parsing table configs
   public ConfigSuccessResponse addConfig(
       String tableConfigsStr,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: (ALL|TASK|UPSERT)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip,
       @DefaultValue("false") @QueryParam("ignoreActiveTasks") boolean 
ignoreActiveTasks,
       @Context HttpHeaders httpHeaders, @Context Request request)
       throws Exception {
+    EnumSet<ValidationType> skipTypes = 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip);
     Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps;
     try {
       tableConfigsAndUnrecognizedProps =
@@ -213,7 +215,7 @@ public class TableConfigsRestletResource {
           Response.Status.BAD_REQUEST);
     }
 
-    validateConfig(tableConfigs, databaseName, typesToSkip);
+    validateConfig(tableConfigs, databaseName, skipTypes);
     tableConfigs.setTableName(rawTableName);
 
     TableConfig offlineTableConfig = tableConfigs.getOffline();
@@ -353,13 +355,14 @@ public class TableConfigsRestletResource {
   public ConfigSuccessResponse updateConfig(
       @ApiParam(value = "TableConfigs name i.e. raw table name", required = 
true) @PathParam("tableName")
       String tableName,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: (ALL|TASK|UPSERT)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip,
       @ApiParam(value = "Reload the table if the new schema is backward 
compatible") @DefaultValue("false")
       @QueryParam("reload") boolean reload,
       @ApiParam(value = "Force update the table schema") 
@DefaultValue("false") @QueryParam("forceTableSchemaUpdate")
       boolean forceTableSchemaUpdate, String tableConfigsStr, @Context 
HttpHeaders headers)
       throws Exception {
+    EnumSet<ValidationType> skipTypes = 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip);
     String databaseName = 
DatabaseUtils.extractDatabaseFromHttpHeaders(headers);
     tableName = DatabaseUtils.translateTableName(tableName, databaseName);
     Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps;
@@ -368,7 +371,7 @@ public class TableConfigsRestletResource {
       tableConfigsAndUnrecognizedProps =
           JsonUtils.stringToObjectAndUnrecognizedProperties(tableConfigsStr, 
TableConfigs.class);
       tableConfigs = tableConfigsAndUnrecognizedProps.getLeft();
-      validateConfig(tableConfigs, databaseName, typesToSkip);
+      validateConfig(tableConfigs, databaseName, skipTypes);
       Preconditions.checkState(
           DatabaseUtils.translateTableName(tableConfigs.getTableName(), 
databaseName).equals(tableName),
           "'tableName' in TableConfigs: %s must match provided tableName: %s", 
tableConfigs.getTableName(), tableName);
@@ -440,8 +443,7 @@ public class TableConfigsRestletResource {
   @ApiOperation(value = "Validate the TableConfigs", notes = "Validate the 
TableConfigs")
   @ManualAuthorization // performed after parsing TableConfigs
   public String validateConfig(String tableConfigsStr,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: "
-          + "(ALL|TASK|UPSERT|TENANT|MINION_INSTANCES)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, 
@Context HttpHeaders httpHeaders,
       @Context Request request) {
     Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps =
@@ -462,8 +464,7 @@ public class TableConfigsRestletResource {
           + "returning the result that would be stored on create/update")
   @ManualAuthorization // performed after parsing TableConfigs
   public String tuneConfig(String tableConfigsStr,
-      @ApiParam(value = "comma separated list of validation type(s) to skip. 
supported types: "
-          + "(ALL|TASK|UPSERT|TENANT|MINION_INSTANCES)")
+      @ApiParam(value = 
TableConfigValidationUtils.VALIDATION_TYPES_TO_SKIP_DESCRIPTION)
       @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, 
@Context HttpHeaders httpHeaders,
       @Context Request request) {
     Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps =
@@ -483,6 +484,7 @@ public class TableConfigsRestletResource {
 
   private Pair<TableConfigs, Map<String, Object>> 
parseAndValidateTableConfigs(String tableConfigsStr,
       @Nullable String typesToSkip, HttpHeaders httpHeaders, Request request) {
+    EnumSet<ValidationType> skipTypes = 
TableConfigValidationUtils.parseValidationTypesToSkip(typesToSkip);
     Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps;
     try {
       tableConfigsAndUnrecognizedProps =
@@ -494,7 +496,7 @@ public class TableConfigsRestletResource {
     }
     String databaseName = 
DatabaseUtils.extractDatabaseFromHttpHeaders(httpHeaders);
     TableConfigs tableConfigs = tableConfigsAndUnrecognizedProps.getLeft();
-    validateConfig(tableConfigs, databaseName, typesToSkip);
+    validateConfig(tableConfigs, databaseName, skipTypes);
     String rawTableName = 
DatabaseUtils.translateTableName(tableConfigs.getTableName(), databaseName);
     tableConfigs.setTableName(rawTableName);
 
@@ -503,7 +505,6 @@ public class TableConfigsRestletResource {
     // equivalent checks inline or via PinotHelixResourceManager). Active-task 
validation is intentionally excluded
     // here: it applies only on the create/update path (gated by the 
ignoreActiveTasks flag) so that validate/tune of an
     // existing table with running tasks is not blocked.
-    Set<TableConfigUtils.ValidationType> skipTypes = 
TableConfigUtils.parseTypesToSkipString(typesToSkip);
     try {
       if (tableConfigs.getOffline() != null) {
         validateClusterAwareConfig(tableConfigs.getOffline(), skipTypes);
@@ -529,14 +530,11 @@ public class TableConfigsRestletResource {
     return tableConfigsAndUnrecognizedProps;
   }
 
-  private void validateClusterAwareConfig(TableConfig tableConfig, 
Set<TableConfigUtils.ValidationType> skipTypes) {
-    if (skipTypes.contains(TableConfigUtils.ValidationType.ALL)) {
-      return;
-    }
-    if (!skipTypes.contains(TableConfigUtils.ValidationType.TENANT)) {
+  private void validateClusterAwareConfig(TableConfig tableConfig, @Nullable 
EnumSet<ValidationType> skipTypes) {
+    if (!ValidationType.TENANT.isSkipped(skipTypes)) {
       _pinotHelixResourceManager.validateTableTenantConfig(tableConfig);
     }
-    if (!skipTypes.contains(TableConfigUtils.ValidationType.MINION_INSTANCES)) 
{
+    if (!ValidationType.MINION_INSTANCES.isSkipped(skipTypes)) {
       
_pinotHelixResourceManager.validateTableTaskMinionInstanceTagConfig(tableConfig);
     }
   }
@@ -550,7 +548,8 @@ public class TableConfigsRestletResource {
 
   /// Validates the provided TableConfigs. Hybrid table validation is 
performed only on the provided
   /// configs and does not check for conflicts with existing tables in the 
cluster.
-  private void validateConfig(TableConfigs tableConfigs, String database, 
@Nullable String typesToSkip) {
+  private void validateConfig(TableConfigs tableConfigs, String database,
+      @Nullable EnumSet<ValidationType> skipTypes) {
     String rawTableName = 
DatabaseUtils.translateTableName(tableConfigs.getTableName(), database);
     TableConfig offlineTableConfig = tableConfigs.getOffline();
     TableConfig realtimeTableConfig = tableConfigs.getRealtime();
@@ -573,9 +572,9 @@ public class TableConfigsRestletResource {
         Preconditions.checkState(offlineRawTableName.equals(rawTableName),
             "Name in 'offline' table config: %s must be equal to 'tableName': 
%s", offlineRawTableName, rawTableName);
         TableConfigUtils.validateTableName(offlineTableConfig);
-        TableConfigUtils.validate(offlineTableConfig, schema, typesToSkip,
+        TableConfigUtils.validate(offlineTableConfig, schema, skipTypes,
             
_pinotHelixResourceManager.getTableConfig(TableNameBuilder.OFFLINE.tableNameWithType(rawTableName)));
-        TaskConfigUtils.validateTaskConfigs(tableConfigs.getOffline(), schema, 
_pinotTaskManager, typesToSkip);
+        TaskConfigUtils.validateTaskConfigs(tableConfigs.getOffline(), schema, 
_pinotTaskManager, skipTypes);
         TableConfigValidatorRegistry.validate(offlineTableConfig, schema);
       }
       if (realtimeTableConfig != null) {
@@ -584,9 +583,9 @@ public class TableConfigsRestletResource {
         Preconditions.checkState(realtimeRawTableName.equals(rawTableName),
             "Name in 'realtime' table config: %s must be equal to 'tableName': 
%s", realtimeRawTableName, rawTableName);
         TableConfigUtils.validateTableName(realtimeTableConfig);
-        TableConfigUtils.validate(realtimeTableConfig, schema, typesToSkip,
+        TableConfigUtils.validate(realtimeTableConfig, schema, skipTypes,
             
_pinotHelixResourceManager.getTableConfig(TableNameBuilder.REALTIME.tableNameWithType(rawTableName)));
-        TaskConfigUtils.validateTaskConfigs(tableConfigs.getRealtime(), 
schema, _pinotTaskManager, typesToSkip);
+        TaskConfigUtils.validateTaskConfigs(tableConfigs.getRealtime(), 
schema, _pinotTaskManager, skipTypes);
         TableConfigValidatorRegistry.validate(realtimeTableConfig, schema);
       }
       if (offlineTableConfig != null && realtimeTableConfig != null) {
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/util/TaskConfigUtils.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/TaskConfigUtils.java
index 53ba3009347..ca7ec801055 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/util/TaskConfigUtils.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/util/TaskConfigUtils.java
@@ -20,10 +20,13 @@ package org.apache.pinot.controller.util;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
+import java.util.EnumSet;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.pinot.controller.helix.core.minion.PinotTaskManager;
 import 
org.apache.pinot.controller.helix.core.minion.generator.PinotTaskGenerator;
 import 
org.apache.pinot.controller.helix.core.minion.generator.TaskGeneratorRegistry;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableTaskConfig;
 import org.apache.pinot.spi.data.Schema;
@@ -33,22 +36,21 @@ import org.slf4j.LoggerFactory;
 
 
 public class TaskConfigUtils {
-  private static final Logger LOGGER = 
LoggerFactory.getLogger(TaskConfigUtils.class);
-  private static final String SCHEDULE_KEY = "schedule";
-  private static final String ALL_VALIDATION_TYPE = "ALL";
-  private static final String TASK_VALIDATION_TYPE = "TASK";
-
   private TaskConfigUtils() {
   }
 
-  public static void validateTaskConfigs(TableConfig tableConfig, Schema 
schema, PinotTaskManager pinotTaskManager,
-      String validationTypesToSkip) {
-    if (tableConfig == null || tableConfig.getTaskConfig() == null) {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(TaskConfigUtils.class);
+  private static final String SCHEDULE_KEY = "schedule";
+
+  /// Validates the task configs of the table with the task generators of the 
task manager. Skips the validation when
+  /// the table has no task config, the task manager is `null`, or the task 
validation is skipped.
+  public static void validateTaskConfigs(TableConfig tableConfig, Schema 
schema,
+      @Nullable PinotTaskManager pinotTaskManager, @Nullable 
EnumSet<ValidationType> skipTypes) {
+    if (tableConfig == null || tableConfig.getTaskConfig() == null || 
pinotTaskManager == null) {
       return;
     }
-    if (ALL_VALIDATION_TYPE.equalsIgnoreCase(validationTypesToSkip) || 
TASK_VALIDATION_TYPE.equalsIgnoreCase(
-        validationTypesToSkip)) {
-      LOGGER.info("Skipping task validation as validationTypesToSkip is set 
to: {}", validationTypesToSkip);
+    if (ValidationType.TASK.isSkipped(skipTypes)) {
+      LOGGER.info("Skipping task validation as validationTypesToSkip is set 
to: {}", skipTypes);
       return;
     }
     TableTaskConfig taskConfig = tableConfig.getTaskConfig();
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
index f3b332271c1..f1577d559ca 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
@@ -894,13 +894,6 @@ public class TableConfigsRestletResourceTest extends 
ControllerTest {
         
adminClient.getTableClient().validateTableConfigs(tableConfigs.toPrettyJsonString(),
 "MINION_INSTANCES");
     Assert.assertNotNull(responseWithMinionSkip);
 
-    // ACTIVE_TASKS is still accepted as a skip type for backward 
compatibility, but the validate/tune preflight
-    // endpoints no longer run active-task validation (it applies only on the 
create/update path), so passing it is a
-    // no-op here.
-    String responseWithTasksSkip =
-        
adminClient.getTableClient().validateTableConfigs(tableConfigs.toPrettyJsonString(),
 "ACTIVE_TASKS");
-    Assert.assertNotNull(responseWithTasksSkip);
-
     // Test validation with multiple skip types
     String responseWithMultipleSkips = adminClient.getTableClient()
         .validateTableConfigs(tableConfigs.toPrettyJsonString(), 
"TENANT,MINION_INSTANCES");
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/util/TaskConfigUtilsTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/util/TaskConfigUtilsTest.java
index bd06d422c55..2bdefb36ee9 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/util/TaskConfigUtilsTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/util/TaskConfigUtilsTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.controller.util;
 
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +27,7 @@ import 
org.apache.pinot.controller.helix.core.minion.generator.BaseTaskGenerator
 import 
org.apache.pinot.controller.helix.core.minion.generator.PinotTaskGenerator;
 import 
org.apache.pinot.controller.helix.core.minion.generator.TaskGeneratorRegistry;
 import org.apache.pinot.core.minion.PinotTaskConfig;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableTaskConfig;
 import org.apache.pinot.spi.config.table.TableType;
@@ -91,6 +93,20 @@ public class TaskConfigUtilsTest {
     TaskConfigUtils.validateTaskConfigs(tableConfig, new Schema(), 
_mockTaskManager, null);
   }
 
+  @Test
+  public void testValidateTableTaskConfigsSkipped() {
+    TableTaskConfig tableTaskConfig =
+        new TableTaskConfig(Map.of(TEST_TASK_TYPE, Map.of("schedule", "0 */10 
* ? * * *")));
+    TableConfig tableConfig =
+        new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TEST_TABLE_NAME).setTaskConfig(tableTaskConfig).build();
+    // The task generator rejects every task config, so these pass only 
because the task validation is skipped
+    TaskConfigUtils.validateTaskConfigs(tableConfig, new Schema(), 
_mockTaskManager, EnumSet.of(ValidationType.TASK));
+    TaskConfigUtils.validateTaskConfigs(tableConfig, new Schema(), 
_mockTaskManager,
+        EnumSet.of(ValidationType.UPSERT, ValidationType.TASK));
+    TaskConfigUtils.validateTaskConfigs(tableConfig, new Schema(), 
_mockTaskManager, EnumSet.of(ValidationType.ALL));
+    TaskConfigUtils.validateTaskConfigs(tableConfig, new Schema(), null, null);
+  }
+
   @Test
   public void testCommonTaskValidations() {
     // invalid schedule
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index eb07e0c0640..705b218b38f 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -23,9 +23,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
@@ -162,7 +162,7 @@ public final class TableConfigUtils {
     _enforcePoolBasedAssignment = enforcePoolBasedAssignment;
   }
 
-  /// @see TableConfigUtils#validate(TableConfig, Schema, String)
+  /// @see TableConfigUtils#validate(TableConfig, Schema, EnumSet)
   public static void validate(TableConfig tableConfig, Schema schema) {
     validate(tableConfig, schema, null);
   }
@@ -175,17 +175,18 @@ public final class TableConfigUtils {
   /// 5. Field Config List
   /// 6. Instance pool and replica group, if enabled
   ///
+  /// The validation types in `skipTypes` are skipped, or none when it is 
`null`, see [ValidationType].
+  ///
   /// TODO: Add more validations for each section (e.g. validate conditions 
are met for aggregateMetrics)
-  public static void validate(TableConfig tableConfig, Schema schema, 
@Nullable String typesToSkip) {
-    validate(tableConfig, schema, typesToSkip, null);
+  public static void validate(TableConfig tableConfig, Schema schema, 
@Nullable EnumSet<ValidationType> skipTypes) {
+    validate(tableConfig, schema, skipTypes, null);
   }
 
-  public static void validate(TableConfig tableConfig, Schema schema, 
@Nullable String typesToSkip,
+  public static void validate(TableConfig tableConfig, Schema schema, 
@Nullable EnumSet<ValidationType> skipTypes,
       @Nullable TableConfig existingTableConfig) {
     Preconditions.checkArgument(schema != null, "Schema should not be null for 
table: %s", tableConfig.getTableName());
-    Set<ValidationType> skipTypes = parseTypesToSkipString(typesToSkip);
     validateEffectiveTableConfig(tableConfig, schema, skipTypes, 
existingTableConfig);
-    if (skipTypes.contains(ValidationType.ALL) || 
!hasConsumingSegmentTierOverwriteForRealtimeTable(tableConfig)) {
+    if (ValidationType.ALL.isSkipped(skipTypes) || 
!hasConsumingSegmentTierOverwriteForRealtimeTable(tableConfig)) {
       return;
     }
     try {
@@ -198,11 +199,11 @@ public final class TableConfigUtils {
   }
 
   private static void validateEffectiveTableConfig(TableConfig tableConfig, 
Schema schema,
-      Set<ValidationType> skipTypes, @Nullable TableConfig 
existingTableConfig) {
+      @Nullable EnumSet<ValidationType> skipTypes, @Nullable TableConfig 
existingTableConfig) {
     // Sanitize the table config before validation
     sanitize(tableConfig);
 
-    if (skipTypes.contains(ValidationType.ALL)) {
+    if (ValidationType.ALL.isSkipped(skipTypes)) {
       return;
     }
 
@@ -229,8 +230,15 @@ public final class TableConfigUtils {
     validateInstanceAssignmentConfigs(tableConfig);
     validateStrictReplicaGroupRouting(tableConfig);
 
-    if (!skipTypes.contains(ValidationType.UPSERT)) {
-      validateUpsertAndDedupConfig(tableConfig, schema, existingTableConfig);
+    boolean upsertEnabled = tableConfig.isUpsertEnabled();
+    boolean dedupEnabled = tableConfig.isDedupEnabled();
+    Preconditions.checkState(!(upsertEnabled && dedupEnabled),
+        "A table can have either Upsert or Dedup enabled, but not both");
+    if (upsertEnabled && !ValidationType.UPSERT.isSkipped(skipTypes)) {
+      validateUpsertConfig(tableConfig, schema, existingTableConfig);
+    }
+    if (dedupEnabled && !ValidationType.DEDUP.isSkipped(skipTypes)) {
+      validateDedupConfig(tableConfig, schema);
     }
 
     validateTaskConfig(tableConfig);
@@ -278,14 +286,16 @@ public final class TableConfigUtils {
         "Instance pool and replica group configurations must be enabled");
   }
 
-  public static Set<ValidationType> parseTypesToSkipString(@Nullable String 
typesToSkipStr) {
+  /// Parses the comma separated validation types to skip, ignoring case and 
surrounding whitespace. Returns `null` when
+  /// there is nothing to skip, that is when the input is `null` or blank.
+  @Nullable
+  public static EnumSet<ValidationType> parseTypesToSkipString(@Nullable 
String typesToSkipStr) {
     if (StringUtils.isBlank(typesToSkipStr)) {
-      return Set.of();
+      return null;
     }
-    String[] split = StringUtils.split(typesToSkipStr, ',');
-    Set<ValidationType> typesToSkip = 
Sets.newHashSetWithExpectedSize(split.length);
-    for (String type : split) {
-      typesToSkip.add(ValidationType.valueOf(type.trim().toUpperCase()));
+    EnumSet<ValidationType> typesToSkip = EnumSet.noneOf(ValidationType.class);
+    for (String type : StringUtils.split(typesToSkipStr, ',')) {
+      
typesToSkip.add(ValidationType.valueOf(type.trim().toUpperCase(Locale.ROOT)));
     }
     return typesToSkip;
   }
@@ -1058,33 +1068,12 @@ public final class TableConfigUtils {
     }
   }
 
-  /// Validates the upsert- and dedup-related configuration. Returns early 
when neither is enabled; otherwise checks
-  /// the shared requirements (upsert and dedup are mutually exclusive, 
primary keys exist and are single-valued,
-  /// strict replica-group routing, no tenant tag override for REALTIME, 
OFFLINE restrictions) and then, per the enabled
-  /// mode:
-  /// - Upsert: no multi-tier / star-tree, comparison / delete-record / 
out-of-order columns are valid, deleted-keys
-  ///   compaction consistency, post-partial-upsert transforms, 
consistency-mode constraints, and delegates to
-  ///   [#validateTTLForUpsertConfig] and [#validatePartialUpsertStrategies]; 
rejects MD5 when disabled.
-  /// - Dedup: delegates to [#validateTTLForDedupConfig]; rejects MD5 when 
disabled.
+  /// Validates the requirements shared by upsert and dedup tables: the 
primary key columns exist in the schema and are
+  /// single-valued, the routing uses strict replica-group, and a REALTIME 
table has no tenant tag override.
   ///
   /// The segment assignment requirements shared with strict replica-group 
routing (one instance per partition, no
   /// COMPLETED segment relocation) are validated by 
[#validateStrictReplicaGroupRouting].
-  @VisibleForTesting
-  static void validateUpsertAndDedupConfig(TableConfig tableConfig, Schema 
schema) {
-    validateUpsertAndDedupConfig(tableConfig, schema, null);
-  }
-
-  static void validateUpsertAndDedupConfig(TableConfig tableConfig, Schema 
schema,
-      @Nullable TableConfig existingTableConfig) {
-    boolean upsertEnabled = tableConfig.isUpsertEnabled();
-    boolean dedupEnabled = tableConfig.isDedupEnabled();
-    if (!upsertEnabled && !dedupEnabled) {
-      return;
-    }
-
-    // check both upsert and dedup are not enabled simultaneously
-    Preconditions.checkState(!(upsertEnabled && dedupEnabled),
-        "A table can have either Upsert or Dedup enabled, but not both");
+  private static void validateUpsertAndDedupCommonConfig(TableConfig 
tableConfig, Schema schema) {
     // primary key exists
     
Preconditions.checkState(CollectionUtils.isNotEmpty(schema.getPrimaryKeyColumns()),
         "Upsert/Dedup table must have primary key columns in the schema");
@@ -1100,18 +1089,30 @@ public final class TableConfigUtils {
     Preconditions.checkState(
         tableConfig.getRoutingConfig() != null && 
isRoutingStrategyAllowedForUpsert(tableConfig.getRoutingConfig()),
         "Upsert/Dedup table must use strict replica-group (i.e. 
strictReplicaGroup) based routing");
-
-    UpsertConfig upsertConfig = tableConfig.getUpsertConfig();
-    DedupConfig dedupConfig = tableConfig.getDedupConfig();
     if (tableConfig.getTableType() == TableType.REALTIME) {
       
Preconditions.checkState(tableConfig.getTenantConfig().getTagOverrideConfig() 
== null || (
               
tableConfig.getTenantConfig().getTagOverrideConfig().getRealtimeConsuming() == 
null
                   && 
tableConfig.getTenantConfig().getTagOverrideConfig().getRealtimeCompleted() == 
null),
           "Invalid tenant tag override used for Upsert/Dedup table");
-    } else {
-      Preconditions.checkState(!dedupEnabled,
-          "Dedup is not supported for OFFLINE table. Only upsert is supported 
for OFFLINE table");
-      assert upsertConfig != null;
+    }
+  }
+
+  /// Validates the upsert config of a table with upsert enabled: checks the 
requirements shared with dedup (see
+  /// [#validateUpsertAndDedupCommonConfig]), the OFFLINE restrictions (no 
partial upsert, segment partition config,
+  /// comparison or time column), no multi-tier / star-tree, comparison / 
delete-record / out-of-order columns are
+  /// valid, deleted-keys compaction consistency, post-partial-upsert 
transforms, consistency-mode constraints, and
+  /// delegates to [#validateTTLForUpsertConfig] and 
[#validatePartialUpsertStrategies]; rejects MD5 when disabled.
+  @VisibleForTesting
+  static void validateUpsertConfig(TableConfig tableConfig, Schema schema) {
+    validateUpsertConfig(tableConfig, schema, null);
+  }
+
+  static void validateUpsertConfig(TableConfig tableConfig, Schema schema, 
@Nullable TableConfig existingTableConfig) {
+    UpsertConfig upsertConfig = tableConfig.getUpsertConfig();
+    assert upsertConfig != null;
+    validateUpsertAndDedupCommonConfig(tableConfig, schema);
+
+    if (tableConfig.getTableType() == TableType.OFFLINE) {
       Preconditions.checkState(upsertConfig.getMode() != 
UpsertConfig.Mode.PARTIAL,
           "Partial upsert is not supported for OFFLINE table");
       // Offline upsert tables require segment partition config so that 
segments are assigned to servers
@@ -1126,174 +1127,168 @@ public final class TableConfigUtils {
       // A comparison column is required, same as realtime upsert. It resolves 
to the configured comparison columns, or
       // the table's time column when comparison columns are not set. Unlike 
realtime, offline tables do not require a
       // time column, so reject the case where neither is configured (no 
implicit segment creation time fallback).
-      Preconditions.checkState(
-          CollectionUtils.isNotEmpty(upsertConfig.getComparisonColumns())
+      
Preconditions.checkState(CollectionUtils.isNotEmpty(upsertConfig.getComparisonColumns())
               || tableConfig.getValidationConfig().getTimeColumnName() != null,
           "Offline upsert table must have a comparison column or a time column 
configured");
     }
 
-    if (upsertEnabled) {
-      // specifically for upsert
-      assert upsertConfig != null;
-
-      // Currently, only one tier is allowed for upsert table, as the 
committed segments can't be moved to other tiers.
-      Preconditions.checkState(tableConfig.getTierConfigsList() == null, "The 
upsert table cannot have multi-tiers");
-      // no startree index
-      
Preconditions.checkState(CollectionUtils.isEmpty(tableConfig.getIndexingConfig().getStarTreeIndexConfigs())
-              && !tableConfig.getIndexingConfig().isEnableDefaultStarTree(),
-          "The upsert table cannot have star-tree index.");
+    // Currently, only one tier is allowed for upsert table, as the committed 
segments can't be moved to other tiers.
+    Preconditions.checkState(tableConfig.getTierConfigsList() == null, "The 
upsert table cannot have multi-tiers");
+    // no startree index
+    
Preconditions.checkState(CollectionUtils.isEmpty(tableConfig.getIndexingConfig().getStarTreeIndexConfigs())
+        && !tableConfig.getIndexingConfig().isEnableDefaultStarTree(), "The 
upsert table cannot have star-tree index.");
 
-      // comparison column exists
-      List<String> comparisonColumns = upsertConfig.getComparisonColumns();
-      if (comparisonColumns != null) {
-        for (String column : comparisonColumns) {
-          Preconditions.checkState(schema.hasColumn(column), "The comparison 
column does not exist on schema");
-        }
+    // comparison column exists
+    List<String> comparisonColumns = upsertConfig.getComparisonColumns();
+    if (comparisonColumns != null) {
+      for (String column : comparisonColumns) {
+        Preconditions.checkState(schema.hasColumn(column), "The comparison 
column does not exist on schema");
       }
+    }
 
-      // Delete record column exist and is a BOOLEAN field
-      String deleteRecordColumn = upsertConfig.getDeleteRecordColumn();
-      if (deleteRecordColumn != null) {
-        FieldSpec fieldSpec = schema.getFieldSpecFor(deleteRecordColumn);
-        Preconditions.checkState(fieldSpec != null,
-            String.format("Column %s specified in deleteRecordColumn does not 
exist", deleteRecordColumn));
-        Preconditions.checkState(fieldSpec.isSingleValueField(),
-            String.format("The deleteRecordColumn - %s must be a single-valued 
column", deleteRecordColumn));
-        DataType dataType = fieldSpec.getDataType();
-        Preconditions.checkState(dataType == DataType.BOOLEAN || dataType == 
DataType.STRING || dataType.isNumeric(),
-            String.format("The deleteRecordColumn - %s must be of type: String 
/ Boolean / Numeric",
-                deleteRecordColumn));
-      }
-
-      String outOfOrderRecordColumn = upsertConfig.getOutOfOrderRecordColumn();
-      if (outOfOrderRecordColumn != null) {
-        Preconditions.checkState(!upsertConfig.isDropOutOfOrderRecord(),
-            "outOfOrderRecordColumn and dropOutOfOrderRecord shouldn't exist 
together for upsert table");
-        FieldSpec fieldSpec = schema.getFieldSpecFor(outOfOrderRecordColumn);
-        Preconditions.checkState(
-            fieldSpec != null && fieldSpec.isSingleValueField() && 
fieldSpec.getDataType() == DataType.BOOLEAN,
-            "The outOfOrderRecordColumn must be a single-valued BOOLEAN 
column");
-      }
+    // Delete record column exist and is a BOOLEAN field
+    String deleteRecordColumn = upsertConfig.getDeleteRecordColumn();
+    if (deleteRecordColumn != null) {
+      FieldSpec fieldSpec = schema.getFieldSpecFor(deleteRecordColumn);
+      Preconditions.checkState(fieldSpec != null,
+          String.format("Column %s specified in deleteRecordColumn does not 
exist", deleteRecordColumn));
+      Preconditions.checkState(fieldSpec.isSingleValueField(),
+          String.format("The deleteRecordColumn - %s must be a single-valued 
column", deleteRecordColumn));
+      DataType dataType = fieldSpec.getDataType();
+      Preconditions.checkState(dataType == DataType.BOOLEAN || dataType == 
DataType.STRING || dataType.isNumeric(),
+          String.format("The deleteRecordColumn - %s must be of type: String / 
Boolean / Numeric", deleteRecordColumn));
+    }
 
-      if (upsertConfig.isEnableDeletedKeysCompactionConsistency()) {
-        // enableDeletedKeysCompactionConsistency shouldn't exist with 
metadataTTL
-        Preconditions.checkState(upsertConfig.getMetadataTTL() == 0,
-            "enableDeletedKeysCompactionConsistency and metadataTTL shouldn't 
exist together for upsert table");
+    String outOfOrderRecordColumn = upsertConfig.getOutOfOrderRecordColumn();
+    if (outOfOrderRecordColumn != null) {
+      Preconditions.checkState(!upsertConfig.isDropOutOfOrderRecord(),
+          "outOfOrderRecordColumn and dropOutOfOrderRecord shouldn't exist 
together for upsert table");
+      FieldSpec fieldSpec = schema.getFieldSpecFor(outOfOrderRecordColumn);
+      Preconditions.checkState(
+          fieldSpec != null && fieldSpec.isSingleValueField() && 
fieldSpec.getDataType() == DataType.BOOLEAN,
+          "The outOfOrderRecordColumn must be a single-valued BOOLEAN column");
+    }
 
-        // enableDeletedKeysCompactionConsistency shouldn't exist with preload 
enabled
-        Preconditions.checkState(upsertConfig.getPreload() != 
Enablement.ENABLE,
-            "enableDeletedKeysCompactionConsistency and preload shouldn't 
exist together for upsert table");
+    if (upsertConfig.isEnableDeletedKeysCompactionConsistency()) {
+      // enableDeletedKeysCompactionConsistency shouldn't exist with 
metadataTTL
+      Preconditions.checkState(upsertConfig.getMetadataTTL() == 0,
+          "enableDeletedKeysCompactionConsistency and metadataTTL shouldn't 
exist together for upsert table");
 
-        // enableDeletedKeysCompactionConsistency should exist with 
deletedKeysTTL
-        Preconditions.checkState(upsertConfig.getDeletedKeysTTL() > 0,
-            "enableDeletedKeysCompactionConsistency should exist with 
deletedKeysTTL for upsert table");
+      // enableDeletedKeysCompactionConsistency shouldn't exist with preload 
enabled
+      Preconditions.checkState(upsertConfig.getPreload() != Enablement.ENABLE,
+          "enableDeletedKeysCompactionConsistency and preload shouldn't exist 
together for upsert table");
 
-        // enableDeletedKeysCompactionConsistency should exist with snapshot 
enabled
-        // NOTE: Allow snapshot to be DEFAULT because it might be enabled at 
server level.
-        Preconditions.checkState(upsertConfig.getSnapshot() != 
Enablement.DISABLE,
-            "enableDeletedKeysCompactionConsistency should exist with snapshot 
for upsert table");
+      // enableDeletedKeysCompactionConsistency should exist with 
deletedKeysTTL
+      Preconditions.checkState(upsertConfig.getDeletedKeysTTL() > 0,
+          "enableDeletedKeysCompactionConsistency should exist with 
deletedKeysTTL for upsert table");
 
-        // enableDeletedKeysCompactionConsistency should exist with 
UpsertCompactionTask / UpsertCompactMergeTask
-        TableTaskConfig taskConfig = tableConfig.getTaskConfig();
-        Preconditions.checkState(
-            taskConfig != null && 
(taskConfig.getTaskTypeConfigsMap().containsKey(UPSERT_COMPACTION_TASK_TYPE)
-                || 
taskConfig.getTaskTypeConfigsMap().containsKey(UPSERT_COMPACT_MERGE_TASK_TYPE)),
-            "enableDeletedKeysCompactionConsistency should exist with 
UpsertCompactionTask"
-                + " / UpsertCompactMergeTask for upsert table");
-      }
-
-      // Validate post-partial-upsert transform configs
-      List<TransformConfig> postPartialUpsertTransformConfigs =
-          upsertConfig.getPostPartialUpsertTransformConfigs();
-      if (postPartialUpsertTransformConfigs != null) {
-        List<TransformConfig> existingPostPartialUpsertTransformConfigs =
-            getPostPartialUpsertTransformConfigs(existingTableConfig);
-        Preconditions.checkState(upsertConfig.getMode() == 
UpsertConfig.Mode.PARTIAL,
-            "postPartialUpsertTransformConfigs can only be configured for 
PARTIAL upsert tables");
-        Set<String> primaryKeyColumns = new 
HashSet<>(schema.getPrimaryKeyColumns());
-        Set<String> transformColumns = new HashSet<>();
-        for (TransformConfig transformConfig : 
postPartialUpsertTransformConfigs) {
-          String columnName = transformConfig.getColumnName();
-          String transformFunction = transformConfig.getTransformFunction();
-          if (columnName == null || transformFunction == null) {
-            throw new IllegalStateException(
-                "columnName/transformFunction cannot be null in 
postPartialUpsertTransformConfigs "
-                    + transformConfig);
-          }
-          Preconditions.checkState(!primaryKeyColumns.contains(columnName),
-              "Post-partial-upsert transform cannot target primary key column 
'%s'", columnName);
-          Preconditions.checkState(comparisonColumns == null || 
!comparisonColumns.contains(columnName),
-              "Post-partial-upsert transform cannot target comparison column 
'%s'", columnName);
-          Preconditions.checkState(!columnName.equals(deleteRecordColumn),
-              "Post-partial-upsert transform cannot target delete record 
column '%s'", columnName);
-          Preconditions.checkState(!columnName.equals(outOfOrderRecordColumn),
-              "Post-partial-upsert transform cannot target out-of-order record 
column '%s'", columnName);
-          if (!transformColumns.add(columnName)) {
-            throw new IllegalStateException(
-                "Duplicate post-partial-upsert transform config found for 
column '" + columnName + "'");
-          }
-          Preconditions.checkState(schema.hasColumn(columnName),
-              "The destination column '%s' of the post-partial-upsert 
transform function must be present in the "
-                  + "schema", columnName);
-          if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(transformFunction)) {
-            throw new IllegalStateException(
-                "Groovy transform functions are disabled. Found '" + 
transformFunction + "' for column '"
-                    + columnName + "' in postPartialUpsertTransformConfigs");
-          }
-          try {
-            validateIngestionTransformFunctionVolatility(transformConfig,
-                existingPostPartialUpsertTransformConfigs);
-            FunctionEvaluator expressionEvaluator =
-                
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction);
-            List<String> arguments = expressionEvaluator.getArguments();
-            if (arguments.contains(columnName)) {
-              throw new IllegalStateException(
-                  "Arguments of a post-partial-upsert transform function '" + 
arguments
-                      + "' cannot contain the destination column '" + 
columnName + "'");
-            }
-          } catch (IllegalStateException e) {
-            throw e;
-          } catch (Exception e) {
-            throw new IllegalStateException(
-                "Invalid post-partial-upsert transform function '" + 
transformFunction + "' for column '"
-                    + columnName + "'", e);
+      // enableDeletedKeysCompactionConsistency should exist with snapshot 
enabled
+      // NOTE: Allow snapshot to be DEFAULT because it might be enabled at 
server level.
+      Preconditions.checkState(upsertConfig.getSnapshot() != 
Enablement.DISABLE,
+          "enableDeletedKeysCompactionConsistency should exist with snapshot 
for upsert table");
+
+      // enableDeletedKeysCompactionConsistency should exist with 
UpsertCompactionTask / UpsertCompactMergeTask
+      TableTaskConfig taskConfig = tableConfig.getTaskConfig();
+      Preconditions.checkState(
+          taskConfig != null && 
(taskConfig.getTaskTypeConfigsMap().containsKey(UPSERT_COMPACTION_TASK_TYPE)
+              || 
taskConfig.getTaskTypeConfigsMap().containsKey(UPSERT_COMPACT_MERGE_TASK_TYPE)),
+          "enableDeletedKeysCompactionConsistency should exist with 
UpsertCompactionTask"
+              + " / UpsertCompactMergeTask for upsert table");
+    }
+
+    // Validate post-partial-upsert transform configs
+    List<TransformConfig> postPartialUpsertTransformConfigs = 
upsertConfig.getPostPartialUpsertTransformConfigs();
+    if (postPartialUpsertTransformConfigs != null) {
+      List<TransformConfig> existingPostPartialUpsertTransformConfigs =
+          getPostPartialUpsertTransformConfigs(existingTableConfig);
+      Preconditions.checkState(upsertConfig.getMode() == 
UpsertConfig.Mode.PARTIAL,
+          "postPartialUpsertTransformConfigs can only be configured for 
PARTIAL upsert tables");
+      Set<String> primaryKeyColumns = new 
HashSet<>(schema.getPrimaryKeyColumns());
+      Set<String> transformColumns = new HashSet<>();
+      for (TransformConfig transformConfig : 
postPartialUpsertTransformConfigs) {
+        String columnName = transformConfig.getColumnName();
+        String transformFunction = transformConfig.getTransformFunction();
+        if (columnName == null || transformFunction == null) {
+          throw new IllegalStateException(
+              "columnName/transformFunction cannot be null in 
postPartialUpsertTransformConfigs " + transformConfig);
+        }
+        Preconditions.checkState(!primaryKeyColumns.contains(columnName),
+            "Post-partial-upsert transform cannot target primary key column 
'%s'", columnName);
+        Preconditions.checkState(comparisonColumns == null || 
!comparisonColumns.contains(columnName),
+            "Post-partial-upsert transform cannot target comparison column 
'%s'", columnName);
+        Preconditions.checkState(!columnName.equals(deleteRecordColumn),
+            "Post-partial-upsert transform cannot target delete record column 
'%s'", columnName);
+        Preconditions.checkState(!columnName.equals(outOfOrderRecordColumn),
+            "Post-partial-upsert transform cannot target out-of-order record 
column '%s'", columnName);
+        if (!transformColumns.add(columnName)) {
+          throw new IllegalStateException(
+              "Duplicate post-partial-upsert transform config found for column 
'" + columnName + "'");
+        }
+        Preconditions.checkState(schema.hasColumn(columnName),
+            "The destination column '%s' of the post-partial-upsert transform 
function must be present in the "
+                + "schema", columnName);
+        if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(transformFunction)) {
+          throw new IllegalStateException(
+              "Groovy transform functions are disabled. Found '" + 
transformFunction + "' for column '" + columnName
+                  + "' in postPartialUpsertTransformConfigs");
+        }
+        try {
+          validateIngestionTransformFunctionVolatility(transformConfig, 
existingPostPartialUpsertTransformConfigs);
+          FunctionEvaluator expressionEvaluator = 
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction);
+          List<String> arguments = expressionEvaluator.getArguments();
+          if (arguments.contains(columnName)) {
+            throw new IllegalStateException("Arguments of a 
post-partial-upsert transform function '" + arguments
+                + "' cannot contain the destination column '" + columnName + 
"'");
           }
+        } catch (IllegalStateException e) {
+          throw e;
+        } catch (Exception e) {
+          throw new IllegalStateException(
+              "Invalid post-partial-upsert transform function '" + 
transformFunction + "' for column '" + columnName
+                  + "'", e);
         }
       }
+    }
 
-      if (upsertConfig.getConsistencyMode() != 
UpsertConfig.ConsistencyMode.NONE) {
-        Preconditions.checkState(upsertConfig.getNewSegmentTrackingTimeMs() > 
0,
-            "Positive newSegmentTrackingTimeMs is required to enable 
consistency mode: "
-                + upsertConfig.getConsistencyMode());
-        // dropOutOfOrderRecord and outOfOrderRecordColumn are not supported 
with SYNC/SNAPSHOT consistency mode
-        // because records must be indexed before metadata is updated. By the 
time we know if a record is
-        // out-of-order, it's already indexed - As of today, we can't skip 
indexing or update the
-        // out-of-order column value.
-        Preconditions.checkState(!upsertConfig.isDropOutOfOrderRecord(),
-            "dropOutOfOrderRecord cannot be enabled when consistencyMode is 
%s. "
-                + "Out-of-order records can only be dropped in NONE 
consistency mode.",
-            upsertConfig.getConsistencyMode());
-        Preconditions.checkState(upsertConfig.getOutOfOrderRecordColumn() == 
null,
-            "outOfOrderRecordColumn cannot be configured when consistencyMode 
is %s. "
-                + "Out-of-order record marking is only supported in NONE 
consistency mode.",
-            upsertConfig.getConsistencyMode());
-      }
-
-      validateTTLForUpsertConfig(tableConfig, schema);
-      validatePartialUpsertStrategies(tableConfig, schema);
-      validateConsumptionDuringUpsertRevert(tableConfig);
-      Preconditions.checkState(
-          !(PinotMd5Mode.isPinotMd5Disabled() && 
upsertConfig.getHashFunction() == HashFunction.MD5),
-          "Upsert hash function MD5 is disabled via '%s=true'", 
CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED);
-    } else {
-      // specifically for dedup
-      assert dedupConfig != null;
-
-      validateTTLForDedupConfig(tableConfig, schema);
-      Preconditions.checkState(
-          !(PinotMd5Mode.isPinotMd5Disabled() && dedupConfig.getHashFunction() 
== HashFunction.MD5),
-          "Dedup hash function MD5 is disabled via '%s=true'", 
CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED);
+    if (upsertConfig.getConsistencyMode() != 
UpsertConfig.ConsistencyMode.NONE) {
+      Preconditions.checkState(upsertConfig.getNewSegmentTrackingTimeMs() > 0,
+          "Positive newSegmentTrackingTimeMs is required to enable consistency 
mode: "
+              + upsertConfig.getConsistencyMode());
+      // dropOutOfOrderRecord and outOfOrderRecordColumn are not supported 
with SYNC/SNAPSHOT consistency mode
+      // because records must be indexed before metadata is updated. By the 
time we know if a record is
+      // out-of-order, it's already indexed - As of today, we can't skip 
indexing or update the
+      // out-of-order column value.
+      Preconditions.checkState(!upsertConfig.isDropOutOfOrderRecord(),
+          "dropOutOfOrderRecord cannot be enabled when consistencyMode is %s. "
+              + "Out-of-order records can only be dropped in NONE consistency 
mode.",
+          upsertConfig.getConsistencyMode());
+      Preconditions.checkState(upsertConfig.getOutOfOrderRecordColumn() == 
null,
+          "outOfOrderRecordColumn cannot be configured when consistencyMode is 
%s. "
+              + "Out-of-order record marking is only supported in NONE 
consistency mode.",
+          upsertConfig.getConsistencyMode());
     }
+
+    validateTTLForUpsertConfig(tableConfig, schema);
+    validatePartialUpsertStrategies(tableConfig, schema);
+    validateConsumptionDuringUpsertRevert(tableConfig);
+    Preconditions.checkState(!(PinotMd5Mode.isPinotMd5Disabled() && 
upsertConfig.getHashFunction() == HashFunction.MD5),
+        "Upsert hash function MD5 is disabled via '%s=true'", 
CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED);
+  }
+
+  /// Validates the dedup config of a table with dedup enabled: checks the 
requirements shared with upsert (see
+  /// [#validateUpsertAndDedupCommonConfig]), rejects OFFLINE tables, 
delegates to [#validateTTLForDedupConfig], and
+  /// rejects MD5 when disabled.
+  @VisibleForTesting
+  static void validateDedupConfig(TableConfig tableConfig, Schema schema) {
+    DedupConfig dedupConfig = tableConfig.getDedupConfig();
+    assert dedupConfig != null;
+    validateUpsertAndDedupCommonConfig(tableConfig, schema);
+    Preconditions.checkState(tableConfig.getTableType() == TableType.REALTIME,
+        "Dedup is not supported for OFFLINE table. Only upsert is supported 
for OFFLINE table");
+
+    validateTTLForDedupConfig(tableConfig, schema);
+    Preconditions.checkState(!(PinotMd5Mode.isPinotMd5Disabled() && 
dedupConfig.getHashFunction() == HashFunction.MD5),
+        "Dedup hash function MD5 is disabled via '%s=true'", 
CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED);
   }
 
   /// Rejects consuming the next segment during a download on tables that 
revert upsert metadata in PROTECTED mode,
@@ -2491,13 +2486,32 @@ public final class TableConfigUtils {
         || upsertConfig.getOutOfOrderRecordColumn() != null);
   }
 
-  // enum of all the skip-able validation types.
-  // ACTIVE_TASKS is retained for backward compatibility: it is still accepted 
as a validationTypesToSkip value so
-  // existing clients do not break, but it is no longer consumed by any 
production path. Active-task validation runs
-  // only on the create/update path (see 
PinotTableRestletResource.tableTasksValidation, gated by ignoreActiveTasks),
-  // not on the validate/tune preflight endpoints, so passing ACTIVE_TASKS 
there is a no-op.
+  /// Validation types that can be skipped through the `validationTypesToSkip` 
parameter. A type is skipped when the
+  /// types to skip contain it or `ALL`, see [#isSkipped].
   public enum ValidationType {
-    ALL, TASK, UPSERT, TENANT, MINION_INSTANCES, ACTIVE_TASKS
+    /// Every type below, and the whole table config validation in 
[TableConfigUtils#validate]
+    ALL,
+    /// Validation of the task configs
+    TASK,
+    /// Validation of the upsert config of an upsert table
+    UPSERT,
+    /// Validation of the dedup config of a dedup table
+    DEDUP,
+    /// Validation of the instance assignment on table creation and update: 
the controller computes the instance
+    /// partitions the table would get from the current instances, and 
validates them
+    INSTANCE_ASSIGNMENT,
+    /// Validation of the tenants on the validate and tune endpoints; creating 
or updating a table always validates
+    /// them
+    TENANT,
+    /// Validation of the minion instances for the task configs on the 
validate and tune endpoints; creating or
+    /// updating a table always validates them
+    MINION_INSTANCES;
+
+    /// Returns whether this validation type is skipped, that is whether the 
given types to skip contain it or `ALL`.
+    /// `null` skips nothing.
+    public boolean isSkipped(@Nullable EnumSet<ValidationType> skipTypes) {
+      return skipTypes != null && (skipTypes.contains(this) || 
skipTypes.contains(ALL));
+    }
   }
 
   /// needsEmptySegmentPruner checks if EmptySegmentPruner is needed for a 
TableConfig.
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index ad89745e377..9fe062216e6 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.base.Throwables;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -30,6 +31,7 @@ import java.util.Set;
 import java.util.function.BiConsumer;
 import javax.annotation.Nullable;
 import org.apache.pinot.common.tier.TierFactory;
+import org.apache.pinot.segment.local.utils.TableConfigUtils.ValidationType;
 import org.apache.pinot.segment.spi.AggregationFunctionType;
 import org.apache.pinot.segment.spi.Constants;
 import 
org.apache.pinot.segment.spi.index.startree.AggregationFunctionColumnPair;
@@ -2620,14 +2622,14 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension("myCol", DataType.STRING)
         .setPrimaryKeyColumns(List.of("myCol"))
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, 
validSchema);
+    TableConfigUtils.validateDedupConfig(validTableConfig, validSchema);
 
     // OFFLINE table should fail because dedup is not yet supported for 
offline tables
     TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
         .setDedupConfig(new 
DedupConfig()).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateDedupConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Dedup is not supported for OFFLINE table. 
Only upsert is supported for OFFLINE"
@@ -2637,7 +2639,7 @@ public class TableConfigUtilsTest {
     Schema schema =
         new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addSingleValueDimension("myCol",
 DataType.STRING).build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, schema);
+      TableConfigUtils.validateDedupConfig(validTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Upsert/Dedup table must have primary key 
columns in the schema");
@@ -2648,7 +2650,7 @@ public class TableConfigUtilsTest {
         .setPrimaryKeyColumns(List.of("myCol"))
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, schema);
+      TableConfigUtils.validateDedupConfig(validTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Upsert/Dedup primary key column: myCol 
cannot be of multi-value type");
@@ -2658,7 +2660,7 @@ public class TableConfigUtilsTest {
         .setDedupConfig(new DedupConfig())
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateDedupConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
@@ -2672,20 +2674,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .setStarTreeIndexConfigs(List.of(starTreeIndexConfig))
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
-
-    // Dedup and upsert can't be enabled simultaneously
-    tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
-        .setDedupConfig(new DedupConfig())
-        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
-        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
-        .build();
-    try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
-      fail();
-    } catch (IllegalStateException e) {
-      assertEquals(e.getMessage(), "A table can have either Upsert or Dedup 
enabled, but not both");
-    }
+    TableConfigUtils.validateDedupConfig(tableConfig, validSchema);
 
     // Valid TIMESTAMP dedupTimeColumn
     DedupConfig dedupConfig = new DedupConfig();
@@ -2700,7 +2689,7 @@ public class TableConfigUtilsTest {
         
.setDedupConfig(dedupConfig).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     // Should not throw an exception - TIMESTAMP is a valid type for 
dedupTimeColumn
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+    TableConfigUtils.validateDedupConfig(tableConfig, schema);
   }
 
   @Test
@@ -2722,7 +2711,7 @@ public class TableConfigUtilsTest {
         .setStreamConfigs(streamConfigs)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateDedupConfig(tableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
@@ -2776,7 +2765,7 @@ public class TableConfigUtilsTest {
       TableConfig tableConfig =
           createTableConfigWithConsumptionPolicy(dropOutOfOrderConfig, 
ParallelSegmentConsumptionPolicy.ALLOW_ALWAYS);
       IllegalStateException e = expectThrows(IllegalStateException.class,
-          () -> TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, 
schema));
+          () -> TableConfigUtils.validateUpsertConfig(tableConfig, schema));
       assertTrue(e.getMessage().endsWith("Set parallelSegmentConsumptionPolicy 
to DISALLOW_ALWAYS"), e.getMessage());
     } finally {
       consistencyModeListener.reset();
@@ -2833,7 +2822,7 @@ public class TableConfigUtilsTest {
     try {
       PinotMd5Mode.setPinotMd5Disabled(true);
       IllegalStateException exception =
-          expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validateUpsertAndDedupConfig(tableConfig,
+          expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validateUpsertConfig(tableConfig,
               schema));
       
assertTrue(exception.getMessage().contains(CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED));
     } finally {
@@ -2857,7 +2846,7 @@ public class TableConfigUtilsTest {
     try {
       PinotMd5Mode.setPinotMd5Disabled(true);
       IllegalStateException exception =
-          expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validateUpsertAndDedupConfig(tableConfig,
+          expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validateDedupConfig(tableConfig,
               schema));
       
assertTrue(exception.getMessage().contains(CommonConstants.CONFIG_OF_PINOT_MD5_DISABLED));
     } finally {
@@ -2869,6 +2858,7 @@ public class TableConfigUtilsTest {
   public void testValidateDedupConfigWithMd5DisabledAllowsUpsertModeNoneMd5() {
     Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
         .addSingleValueDimension("myCol", DataType.STRING)
+        .addDateTime(TIME_COLUMN, DataType.LONG, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
         .setPrimaryKeyColumns(List.of("myCol"))
         .build();
     DedupConfig dedupConfig = new DedupConfig();
@@ -2876,12 +2866,13 @@ public class TableConfigUtilsTest {
     UpsertConfig upsertConfig = new UpsertConfig(UpsertConfig.Mode.NONE);
     upsertConfig.setHashFunction(HashFunction.MD5);
     TableConfig tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
         
.setUpsertConfig(upsertConfig).setDedupConfig(dedupConfig).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .setStreamConfigs(getStreamConfigs())
         .build();
     try {
       PinotMd5Mode.setPinotMd5Disabled(true);
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validate(tableConfig, schema);
     } finally {
       PinotMd5Mode.setPinotMd5Disabled(false);
     }
@@ -2902,7 +2893,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension("myCol", DataType.STRING)
         .setPrimaryKeyColumns(List.of("myCol"))
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, 
validSchema);
+    TableConfigUtils.validateUpsertConfig(validTableConfig, validSchema);
 
     // OFFLINE table without segment partition config should fail with 
partition config error
     TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
@@ -2910,7 +2901,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
@@ -2926,7 +2917,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
@@ -2940,7 +2931,7 @@ public class TableConfigUtilsTest {
         .setSegmentPartitionConfig(segmentPartitionConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
 
     // OFFLINE table with partial upsert should fail
     UpsertConfig partialUpsertConfig = new 
UpsertConfig(UpsertConfig.Mode.PARTIAL);
@@ -2950,7 +2941,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Partial upsert is not supported for 
OFFLINE table");
@@ -2960,12 +2951,12 @@ public class TableConfigUtilsTest {
         .setUpsertConfig(upsertConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, 
validSchema);
+    TableConfigUtils.validateUpsertConfig(validTableConfig, validSchema);
 
     Schema schema =
         new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addSingleValueDimension("myCol",
 DataType.STRING).build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(validTableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(validTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Upsert/Dedup table must have primary key 
columns in the schema");
@@ -2974,7 +2965,7 @@ public class TableConfigUtilsTest {
     tableConfig =
         new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME).setUpsertConfig(upsertConfig).build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
@@ -2990,7 +2981,7 @@ public class TableConfigUtilsTest {
         .setTagOverrideConfig(new TagOverrideConfig("T1_REALTIME", 
"T2_REALTIME"))
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail("Tag override must not be allowed with upsert");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Invalid tenant tag override used for 
Upsert/Dedup table");
@@ -3006,7 +2997,7 @@ public class TableConfigUtilsTest {
         .build();
 
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail("Tag override must not be allowed with upsert");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Invalid tenant tag override used for 
Upsert/Dedup table");
@@ -3020,13 +3011,13 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .setTagOverrideConfig(new TagOverrideConfig(null, null))
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
 
     tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
         .setUpsertConfig(upsertConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
 
     StarTreeIndexConfig starTreeIndexConfig = new 
StarTreeIndexConfig(List.of("myCol"), null,
         List.of(new 
AggregationFunctionColumnPair(AggregationFunctionType.COUNT, 
"myCol").toColumnName()), null, 10);
@@ -3036,7 +3027,7 @@ public class TableConfigUtilsTest {
         .setStarTreeIndexConfigs(List.of(starTreeIndexConfig))
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, validSchema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, validSchema);
       fail();
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "The upsert table cannot have star-tree 
index.");
@@ -3108,7 +3099,7 @@ public class TableConfigUtilsTest {
         .addMultiValueDimension(mvCol, DataType.STRING)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       fail("Shouldn't fail table creation when delete column type is 
single-valued.");
     }
@@ -3120,7 +3111,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       fail("Shouldn't fail table creation when delete column type is 
single-valued.");
     }
@@ -3132,7 +3123,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Should have failed table creation when delete column type is 
timestamp.");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "The deleteRecordColumn - timestampCol must 
be of type: String / Boolean / Numeric");
@@ -3145,7 +3136,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Should have failed table creation when invalid delete column 
entered.");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Column invalidCol specified in 
deleteRecordColumn does not exist");
@@ -3158,7 +3149,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Should have failed table creation when delete column type is 
multi-valued.");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "The deleteRecordColumn - mvCol must be a 
single-valued column");
@@ -3180,7 +3171,7 @@ public class TableConfigUtilsTest {
         .addMultiValueDimension(mvCol, DataType.STRING)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Should fail table creation when PK column type is multi-valued.");
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Upsert/Dedup primary key column: myPkCol 
cannot be of multi-value type");
@@ -3202,7 +3193,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension(delCol, DataType.BOOLEAN)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "Deleted Keys TTL can only be enabled with 
deleteRecordColumn set.");
     }
@@ -3222,7 +3213,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension(delCol, DataType.BOOLEAN)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "MetadataTTL / DeletedKeysTTL does not work 
with multiple comparison columns");
     }
@@ -3234,7 +3225,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "MetadataTTL / DeletedKeysTTL must have comparison column: myCol in 
numeric type, found: STRING");
@@ -3246,7 +3237,7 @@ public class TableConfigUtilsTest {
         .setUpsertConfig(upsertConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, schema);
 
     // upsert out-of-order configs
     String outOfOrderRecordColumn = "outOfOrderRecordColumn";
@@ -3265,7 +3256,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension(outOfOrderRecordColumn, DataType.BOOLEAN)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "outOfOrderRecordColumn and dropOutOfOrderRecord shouldn't exist 
together for upsert table");
@@ -3285,7 +3276,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension(outOfOrderRecordColumn, DataType.STRING)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "The outOfOrderRecordColumn must be a 
single-valued BOOLEAN column");
     }
@@ -3305,7 +3296,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension("myPkCol", DataType.STRING)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Expected IllegalStateException for dropOutOfOrderRecord with SYNC 
consistency mode");
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("dropOutOfOrderRecord cannot be 
enabled when consistencyMode is SYNC"));
@@ -3321,7 +3312,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Expected IllegalStateException for dropOutOfOrderRecord with 
SNAPSHOT consistency mode");
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("dropOutOfOrderRecord cannot be 
enabled when consistencyMode is SNAPSHOT"));
@@ -3335,7 +3326,7 @@ public class TableConfigUtilsTest {
         .setUpsertConfig(upsertConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, schema);
 
     // test outOfOrderRecordColumn cannot be enabled with SYNC consistency mode
     upsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
@@ -3353,7 +3344,7 @@ public class TableConfigUtilsTest {
         .addSingleValueDimension("isOutOfOrder", DataType.BOOLEAN)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Expected IllegalStateException for outOfOrderRecordColumn with 
SYNC consistency mode");
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("outOfOrderRecordColumn cannot be 
configured when consistencyMode is SYNC"));
@@ -3369,7 +3360,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
       fail("Expected IllegalStateException for outOfOrderRecordColumn with 
SNAPSHOT consistency mode");
     } catch (IllegalStateException e) {
       assertTrue(
@@ -3384,7 +3375,7 @@ public class TableConfigUtilsTest {
         .setUpsertConfig(upsertConfig)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, schema);
 
     // test enableDeletedKeysCompactionConsistency shouldn't exist with 
metadataTTL
     upsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
@@ -3395,7 +3386,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "enableDeletedKeysCompactionConsistency and metadataTTL shouldn't 
exist together for upsert table");
@@ -3410,7 +3401,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "enableDeletedKeysCompactionConsistency and preload shouldn't exist 
together for upsert table");
@@ -3425,7 +3416,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "enableDeletedKeysCompactionConsistency should exist with 
deletedKeysTTL for upsert table");
@@ -3441,7 +3432,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(),
           "enableDeletedKeysCompactionConsistency should exist with snapshot 
for upsert table");
@@ -3457,7 +3448,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(tableConfig, schema);
     } catch (IllegalStateException e) {
       assertEquals(e.getMessage(), "enableDeletedKeysCompactionConsistency 
should exist with UpsertCompactionTask "
           + "/ UpsertCompactMergeTask for upsert table");
@@ -4795,6 +4786,65 @@ public class TableConfigUtilsTest {
     assertNull(TableConfigUtils.getPartitionColumn(tableConfig));
   }
 
+  @Test
+  public void testParseTypesToSkipString() {
+    assertNull(TableConfigUtils.parseTypesToSkipString(null));
+    assertNull(TableConfigUtils.parseTypesToSkipString(" "));
+    assertEquals(TableConfigUtils.parseTypesToSkipString("task, 
Instance_Assignment"),
+        EnumSet.of(ValidationType.TASK, ValidationType.INSTANCE_ASSIGNMENT));
+    expectThrows(IllegalArgumentException.class, () -> 
TableConfigUtils.parseTypesToSkipString("TASK,UNKNOWN"));
+
+    // A validation type is skipped when the types to skip contain it or ALL
+    assertTrue(ValidationType.TASK.isSkipped(EnumSet.of(ValidationType.TASK)));
+    assertTrue(ValidationType.TASK.isSkipped(EnumSet.of(ValidationType.ALL)));
+    
assertFalse(ValidationType.TASK.isSkipped(EnumSet.of(ValidationType.UPSERT)));
+    assertFalse(ValidationType.TASK.isSkipped(null));
+  }
+
+  @Test
+  public void testValidateSkipUpsertAndDedup() {
+    // Upsert and dedup tables require primary key columns, which the schema 
does not have
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addDateTime(TIME_COLUMN, DataType.LONG, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
+        .build();
+    String noPrimaryKeyMessage = "Upsert/Dedup table must have primary key 
columns in the schema";
+
+    TableConfig upsertTableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
+        .setStreamConfigs(getStreamConfigs())
+        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
+        .build();
+    IllegalStateException e = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(upsertTableConfig, schema, 
EnumSet.of(ValidationType.DEDUP)));
+    assertEquals(e.getMessage(), noPrimaryKeyMessage);
+    TableConfigUtils.validate(upsertTableConfig, schema, 
EnumSet.of(ValidationType.UPSERT));
+
+    TableConfig dedupTableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
+        .setStreamConfigs(getStreamConfigs())
+        .setDedupConfig(new DedupConfig())
+        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
+        .build();
+    e = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(dedupTableConfig, schema, 
EnumSet.of(ValidationType.UPSERT)));
+    assertEquals(e.getMessage(), noPrimaryKeyMessage);
+    TableConfigUtils.validate(dedupTableConfig, schema, 
EnumSet.of(ValidationType.DEDUP));
+
+    // A table cannot enable both upsert and dedup, even when both validations 
are skipped
+    TableConfig upsertAndDedupTableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
+        .setStreamConfigs(getStreamConfigs())
+        .setUpsertConfig(new UpsertConfig(UpsertConfig.Mode.FULL))
+        .setDedupConfig(new DedupConfig())
+        .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
+        .build();
+    EnumSet<ValidationType> skipUpsertAndDedup = 
EnumSet.of(ValidationType.UPSERT, ValidationType.DEDUP);
+    e = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(upsertAndDedupTableConfig, schema, 
skipUpsertAndDedup));
+    assertEquals(e.getMessage(), "A table can have either Upsert or Dedup 
enabled, but not both");
+  }
+
   @DataProvider(name = "tableTypeTestDataProvider")
   public Object[][] tableTypeTestDataProvider() {
     return new Object[][]{
@@ -5025,7 +5075,7 @@ public class TableConfigUtilsTest {
         .setStreamConfigs(streamConfigs)
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
-    TableConfigUtils.validateUpsertAndDedupConfig(tableConfig, schema);
+    TableConfigUtils.validateUpsertConfig(tableConfig, schema);
 
     // postPartialUpsertTransformConfigs on FULL upsert should fail
     UpsertConfig fullUpsertConfig = new UpsertConfig(UpsertConfig.Mode.FULL);
@@ -5038,7 +5088,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(fullTableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(fullTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("PARTIAL"));
@@ -5054,7 +5104,7 @@ public class TableConfigUtilsTest {
         
.setUpsertConfig(dupConfig).setStreamConfigs(streamConfigs).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(dupTableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(dupTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("Duplicate"));
@@ -5071,7 +5121,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(missingColTableConfig, 
schema);
+      TableConfigUtils.validateUpsertConfig(missingColTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("must be present in the schema"));
@@ -5086,7 +5136,7 @@ public class TableConfigUtilsTest {
         
.setUpsertConfig(selfRefConfig).setStreamConfigs(streamConfigs).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(selfRefTableConfig, 
schema);
+      TableConfigUtils.validateUpsertConfig(selfRefTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("cannot contain the destination 
column"));
@@ -5101,7 +5151,7 @@ public class TableConfigUtilsTest {
         
.setUpsertConfig(pkConfig).setStreamConfigs(streamConfigs).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(pkTableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(pkTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("primary key column"));
@@ -5117,7 +5167,7 @@ public class TableConfigUtilsTest {
         
.setUpsertConfig(compConfig).setStreamConfigs(streamConfigs).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(compTableConfig, schema);
+      TableConfigUtils.validateUpsertConfig(compTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("comparison column"));
@@ -5135,7 +5185,7 @@ public class TableConfigUtilsTest {
         .setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(deleteColTableConfig, 
schema);
+      TableConfigUtils.validateUpsertConfig(deleteColTableConfig, schema);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("delete record column"));
@@ -5160,7 +5210,7 @@ public class TableConfigUtilsTest {
         
.setUpsertConfig(oooColConfig).setStreamConfigs(streamConfigs).setRoutingConfig(STRICT_REPLICA_ROUTING_CONFIG)
         .build();
     try {
-      TableConfigUtils.validateUpsertAndDedupConfig(oooColTableConfig, 
schemaWithBool);
+      TableConfigUtils.validateUpsertConfig(oooColTableConfig, schemaWithBool);
       fail();
     } catch (IllegalStateException e) {
       assertTrue(e.getMessage().contains("out-of-order record column"));
@@ -5179,24 +5229,24 @@ public class TableConfigUtilsTest {
 
     TableConfig nonDeterministicConfig = 
buildPostPartialUpsertTransformTableConfig(streamConfigs, "now()");
     IllegalStateException nonDeterministicError = 
expectThrows(IllegalStateException.class,
-        () -> 
TableConfigUtils.validateUpsertAndDedupConfig(nonDeterministicConfig, schema));
+        () -> TableConfigUtils.validateUpsertConfig(nonDeterministicConfig, 
schema));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'now' has 
VOLATILE volatility"));
 
     TableConfig existingConfig = 
buildPostPartialUpsertTransformTableConfig(streamConfigs, "now()");
-    TableConfigUtils.validateUpsertAndDedupConfig(nonDeterministicConfig, 
schema, existingConfig);
+    TableConfigUtils.validateUpsertConfig(nonDeterministicConfig, schema, 
existingConfig);
 
     TableConfig changedConfig = 
buildPostPartialUpsertTransformTableConfig(streamConfigs, "plus(now(), 1)");
     nonDeterministicError = expectThrows(IllegalStateException.class,
-        () -> TableConfigUtils.validateUpsertAndDedupConfig(changedConfig, 
schema, existingConfig));
+        () -> TableConfigUtils.validateUpsertConfig(changedConfig, schema, 
existingConfig));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'now' has 
VOLATILE volatility"));
 
     TableConfig randomConfig = 
buildPostPartialUpsertTransformTableConfig(streamConfigs, "rand()");
     nonDeterministicError = expectThrows(IllegalStateException.class,
-        () -> TableConfigUtils.validateUpsertAndDedupConfig(randomConfig, 
schema));
+        () -> TableConfigUtils.validateUpsertConfig(randomConfig, schema));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'rand' 
has VOLATILE volatility"));
 
     TableConfig seededRandomConfig = 
buildPostPartialUpsertTransformTableConfig(streamConfigs, "rand(123)");
-    TableConfigUtils.validateUpsertAndDedupConfig(seededRandomConfig, schema);
+    TableConfigUtils.validateUpsertConfig(seededRandomConfig, schema);
   }
 
   private static TableConfig 
buildPostPartialUpsertTransformTableConfig(Map<String, String> streamConfigs,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to