Copilot commented on code in PR #18955:
URL: https://github.com/apache/pinot/pull/18955#discussion_r3555658602


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java:
##########
@@ -443,325 +443,371 @@ private static boolean isValidPeerDownloadScheme(String 
peerSegmentDownloadSchem
         || 
CommonConstants.HTTPS_PROTOCOL.equalsIgnoreCase(peerSegmentDownloadScheme);
   }
 
-  /**
-   * Validates the following:
-   * 1. validity of filter function
-   * 2. checks for duplicate transform configs
-   * 3. checks for null column name or transform function in transform config
-   * 4. validity of transform function string
-   * 5. checks for source fields used in destination columns
-   * 6. ingestion type for dimension tables
-   */
+  /// Validates the table's [IngestionConfig] together with the closely 
related metrics-aggregation config, covering:
+  /// - Metrics aggregation (both the `aggregateMetrics` flag and ingestion 
`aggregationConfigs`), delegated to
+  ///   [#validateMetricsAggregation].
+  /// - Batch ingestion: each batch config map is well-formed, and a dimension 
table has batch ingestion configured
+  ///   with `REFRESH` segment ingestion type.
+  /// - Stream ingestion: streams are declared in only one place, at least one 
stream is present, and pauseless
+  ///   consumption has a valid `peerSegmentDownloadScheme`.
+  /// - Filter config: the filter function is valid and not a disabled Groovy 
expression.
+  /// - Source field configs: no source field is duplicated within the same 
complex-type phase.
+  /// - Enrichment configs: each config is valid.
+  /// - Transform configs: non-null column and function, no duplicate 
destination, and each destination is a schema
+  ///   column, an intermediate consumed by another transform, or an 
aggregation source column; the function is valid
+  ///   and does not reference its own destination.
+  /// - Complex-type config: no schema field collides with a 
`prefixesToRename` prefix.
+  /// - Schema-conforming transformer config.
   @VisibleForTesting
   public static void validateIngestionConfig(TableConfig tableConfig, Schema 
schema) {
+    // All metrics-aggregation validation lives here; it returns the columns 
referenced as aggregation sources, which
+    // a transform config is allowed to target as its destination (see the 
transform validation below).
+    Set<String> aggregationSourceColumns = 
validateMetricsAggregation(tableConfig, schema);
+
     IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+    if (ingestionConfig == null) {
+      return;
+    }
 
-    if (ingestionConfig != null) {
-      String tableNameWithType = tableConfig.getTableName();
+    String tableNameWithType = tableConfig.getTableName();
 
-      // Batch
-      if (ingestionConfig.getBatchIngestionConfig() != null) {
-        BatchIngestionConfig cfg = ingestionConfig.getBatchIngestionConfig();
-        List<Map<String, String>> batchConfigMaps = cfg.getBatchConfigMaps();
-        try {
-          if (CollectionUtils.isNotEmpty(batchConfigMaps)) {
-            // Validate that BatchConfig can be created
-            batchConfigMaps.forEach(b -> new BatchConfig(tableNameWithType, 
b));
-          }
-        } catch (Exception e) {
-          throw new IllegalStateException("Could not create BatchConfig using 
the batchConfig map", e);
-        }
-        if (tableConfig.isDimTable()) {
-          
Preconditions.checkState(cfg.getSegmentIngestionType().equalsIgnoreCase("REFRESH"),
-              "Dimension tables must have segment ingestion type REFRESH");
+    // Batch
+    if (ingestionConfig.getBatchIngestionConfig() != null) {
+      BatchIngestionConfig cfg = ingestionConfig.getBatchIngestionConfig();
+      List<Map<String, String>> batchConfigMaps = cfg.getBatchConfigMaps();
+      try {
+        if (CollectionUtils.isNotEmpty(batchConfigMaps)) {
+          // Validate that BatchConfig can be created
+          batchConfigMaps.forEach(b -> new BatchConfig(tableNameWithType, b));
         }
+      } catch (Exception e) {
+        throw new IllegalStateException("Could not create BatchConfig using 
the batchConfig map", e);
       }
       if (tableConfig.isDimTable()) {
-        Preconditions.checkState(ingestionConfig.getBatchIngestionConfig() != 
null,
-            "Dimension tables must have batch ingestion configuration");
-      }
-
-      // Stream
-      // stream config map can either be in ingestion config or indexing 
config. cannot be in both places
-      if (ingestionConfig.getStreamIngestionConfig() != null) {
-        IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
-        Preconditions.checkState(indexingConfig == null || 
MapUtils.isEmpty(indexingConfig.getStreamConfigs()),
-            "Should not use indexingConfig#getStreamConfigs if 
ingestionConfig#StreamIngestionConfig is provided");
-        StreamIngestionConfig streamIngestionConfig = 
ingestionConfig.getStreamIngestionConfig();
-        List<Map<String, String>> streamConfigMaps = 
streamIngestionConfig.getStreamConfigMaps();
-        Preconditions.checkState(!streamConfigMaps.isEmpty(), "Must have at 
least 1 stream in REALTIME table");
-        // TODO: for multiple stream configs, validate them
-
-        boolean isPauselessEnabled = 
streamIngestionConfig.isPauselessConsumptionEnabled();
-        if (isPauselessEnabled) {
-          int replication = tableConfig.getReplication();
-          // We are checking for this only when replication is greater than 1 
because in test environments
-          // users still prefer to create pauseless tables with replication 1
-          if (replication > 1) {
-            String peerSegmentDownloadScheme = 
tableConfig.getValidationConfig().getPeerSegmentDownloadScheme();
-            
Preconditions.checkState(StringUtils.isNotEmpty(peerSegmentDownloadScheme) && 
isValidPeerDownloadScheme(
-                    peerSegmentDownloadScheme),
-                "Must have a valid peerSegmentDownloadScheme set in validation 
config for pauseless consumption");
-          } else {
-            LOGGER.warn("It's not recommended to create pauseless tables with 
replication 1 for stability reasons.");
-          }
-        }
+        
Preconditions.checkState(cfg.getSegmentIngestionType().equalsIgnoreCase("REFRESH"),
+            "Dimension tables must have segment ingestion type REFRESH");
       }
+    }
+    if (tableConfig.isDimTable()) {
+      Preconditions.checkState(ingestionConfig.getBatchIngestionConfig() != 
null,
+          "Dimension tables must have batch ingestion configuration");
+    }
 
-      // Filter config
-      FilterConfig filterConfig = ingestionConfig.getFilterConfig();
-      if (filterConfig != null) {
-        String filterFunction = filterConfig.getFilterFunction();
-        if (filterFunction != null) {
-          if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(filterFunction)) {
-            throw new IllegalStateException(
-                "Groovy filter functions are disabled for table config. Found 
'" + filterFunction + "'");
-          }
-          try {
-            FunctionEvaluatorFactory.getExpressionEvaluator(filterFunction);
-          } catch (Exception e) {
-            throw new IllegalStateException(
-                "Invalid filter function '" + filterFunction + "', exception: 
" + e.getMessage(), e);
-          }
+    // Stream
+    // stream config map can either be in ingestion config or indexing config. 
cannot be in both places
+    if (ingestionConfig.getStreamIngestionConfig() != null) {
+      IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
+      Preconditions.checkState(indexingConfig == null || 
MapUtils.isEmpty(indexingConfig.getStreamConfigs()),
+          "Should not use indexingConfig#getStreamConfigs if 
ingestionConfig#StreamIngestionConfig is provided");
+      StreamIngestionConfig streamIngestionConfig = 
ingestionConfig.getStreamIngestionConfig();
+      List<Map<String, String>> streamConfigMaps = 
streamIngestionConfig.getStreamConfigMaps();
+      Preconditions.checkState(!streamConfigMaps.isEmpty(), "Must have at 
least 1 stream in REALTIME table");
+      // TODO: for multiple stream configs, validate them
+
+      boolean isPauselessEnabled = 
streamIngestionConfig.isPauselessConsumptionEnabled();
+      if (isPauselessEnabled) {
+        int replication = tableConfig.getReplication();
+        // We are checking for this only when replication is greater than 1 
because in test environments
+        // users still prefer to create pauseless tables with replication 1
+        if (replication > 1) {
+          String peerSegmentDownloadScheme = 
tableConfig.getValidationConfig().getPeerSegmentDownloadScheme();
+          Preconditions.checkState(
+              StringUtils.isNotEmpty(peerSegmentDownloadScheme) && 
isValidPeerDownloadScheme(peerSegmentDownloadScheme),
+              "Must have a valid peerSegmentDownloadScheme set in validation 
config for pauseless consumption");
+        } else {
+          LOGGER.warn("It's not recommended to create pauseless tables with 
replication 1 for stability reasons.");
         }
       }
+    }
 
-      // Aggregation configs
-      List<AggregationConfig> aggregationConfigs = 
ingestionConfig.getAggregationConfigs();
-      Set<String> aggregationSourceColumns = new HashSet<>();
-      if (CollectionUtils.isNotEmpty(aggregationConfigs)) {
-        
Preconditions.checkState(!tableConfig.getIndexingConfig().isAggregateMetrics(),
-            "aggregateMetrics cannot be set with AggregationConfig");
-        Set<String> aggregationColumns = new HashSet<>();
-        for (AggregationConfig aggregationConfig : aggregationConfigs) {
-          String columnName = aggregationConfig.getColumnName();
-          String aggregationFunction = 
aggregationConfig.getAggregationFunction();
-          if (columnName == null || aggregationFunction == null) {
-            throw new IllegalStateException(
-                "columnName/aggregationFunction cannot be null in 
AggregationConfig " + aggregationConfig);
-          }
-
-          FieldSpec fieldSpec = schema.getFieldSpecFor(columnName);
-          Preconditions.checkState(fieldSpec != null,
-              "The destination column '" + columnName + "' of the aggregation 
function must be present in the schema");
-          Preconditions.checkState(fieldSpec.getFieldType() == 
FieldSpec.FieldType.METRIC,
-              "The destination column '" + columnName + "' of the aggregation 
function must be a metric column");
-          DataType dataType = fieldSpec.getDataType();
-
-          if (!aggregationColumns.add(columnName)) {
-            throw new IllegalStateException("Duplicate aggregation config 
found for column '" + columnName + "'");
-          }
-          ExpressionContext expressionContext;
-          try {
-            expressionContext = 
RequestContextUtils.getExpression(aggregationConfig.getAggregationFunction());
-          } catch (Exception e) {
-            throw new IllegalStateException(
-                "Invalid aggregation function '" + aggregationFunction + "' 
for column '" + columnName + "'", e);
-          }
-          Preconditions.checkState(expressionContext.getType() == 
ExpressionContext.Type.FUNCTION,
-              "aggregation function must be a function for: %s", 
aggregationConfig);
-
-          FunctionContext functionContext = expressionContext.getFunction();
-          AggregationFunctionType functionType =
-              
AggregationFunctionType.getAggregationFunctionType(functionContext.getFunctionName());
-          List<ExpressionContext> arguments = functionContext.getArguments();
-          int numArguments = arguments.size();
-          if (functionType == DISTINCTCOUNTHLL) {
-            Preconditions.checkState(numArguments >= 1 && numArguments <= 2,
-                "DISTINCT_COUNT_HLL can have at most two arguments: %s", 
aggregationConfig);
-            if (numArguments == 2) {
-              ExpressionContext secondArgument = arguments.get(1);
-              Preconditions.checkState(secondArgument.getType() == 
ExpressionContext.Type.LITERAL,
-                  "Second argument of DISTINCT_COUNT_HLL must be literal: %s", 
aggregationConfig);
-              String literal = secondArgument.getLiteral().getStringValue();
-              Preconditions.checkState(StringUtils.isNumeric(literal),
-                  "Second argument of DISTINCT_COUNT_HLL must be a number: 
%s", aggregationConfig);
-            }
-            Preconditions.checkState(dataType == DataType.BYTES, "Result type 
for DISTINCT_COUNT_HLL must be BYTES: %s",
-                aggregationConfig);
-          } else if (functionType == DISTINCTCOUNTHLLPLUS) {
-            Preconditions.checkState(numArguments >= 1 && numArguments <= 3,
-                "DISTINCT_COUNT_HLL_PLUS can have at most three arguments: 
%s", aggregationConfig);
-            if (numArguments == 2) {
-              ExpressionContext secondArgument = arguments.get(1);
-              Preconditions.checkState(secondArgument.getType() == 
ExpressionContext.Type.LITERAL,
-                  "Second argument of DISTINCT_COUNT_HLL_PLUS must be literal: 
%s", aggregationConfig);
-              String literal = secondArgument.getLiteral().getStringValue();
-              Preconditions.checkState(StringUtils.isNumeric(literal),
-                  "Second argument of DISTINCT_COUNT_HLL_PLUS must be a 
number: %s", aggregationConfig);
-            }
-            if (numArguments == 3) {
-              ExpressionContext thirdArgument = arguments.get(2);
-              Preconditions.checkState(thirdArgument.getType() == 
ExpressionContext.Type.LITERAL,
-                  "Third argument of DISTINCT_COUNT_HLL_PLUS must be literal: 
%s", aggregationConfig);
-              String literal = thirdArgument.getLiteral().getStringValue();
-              Preconditions.checkState(StringUtils.isNumeric(literal),
-                  "Third argument of DISTINCT_COUNT_HLL_PLUS must be a number: 
%s", aggregationConfig);
-            }
-            Preconditions.checkState(dataType == DataType.BYTES,
-                "Result type for DISTINCT_COUNT_HLL_PLUS must be BYTES: %s", 
aggregationConfig);
-          } else if (functionType == SUMPRECISION) {
-            Preconditions.checkState(numArguments >= 2 && numArguments <= 3,
-                "SUM_PRECISION must specify precision (required), scale 
(optional): %s", aggregationConfig);
-            ExpressionContext secondArgument = arguments.get(1);
-            Preconditions.checkState(secondArgument.getType() == 
ExpressionContext.Type.LITERAL,
-                "Second argument of SUM_PRECISION must be literal: %s", 
aggregationConfig);
-            String literal = secondArgument.getLiteral().getStringValue();
-            Preconditions.checkState(StringUtils.isNumeric(literal),
-                "Second argument of SUM_PRECISION must be a number: %s", 
aggregationConfig);
-            Preconditions.checkState(dataType == DataType.BIG_DECIMAL || 
dataType == DataType.BYTES,
-                "Result type for SUM_PRECISION must be BIG_DECIMAL or BYTES: 
%s", aggregationConfig);
-          } else {
-            Preconditions.checkState(numArguments == 1, "%s can only have one 
argument: %s", functionType,
-                aggregationConfig);
-          }
-          ExpressionContext firstArgument = arguments.get(0);
-          Preconditions.checkState(firstArgument.getType() == 
ExpressionContext.Type.IDENTIFIER,
-              "First argument of aggregation function: %s must be identifier, 
got: %s", functionType,
-              firstArgument.getType());
-          // Create a ValueAggregator for the aggregation function and check 
if it is supported for ingestion (fixed
-          // size aggregated value).
-          ValueAggregator<?, ?> valueAggregator;
-          try {
-            valueAggregator =
-                ValueAggregatorFactory.getValueAggregator(functionType, 
arguments.subList(1, numArguments));
-          } catch (Exception e) {
-            throw new IllegalStateException(
-                "Caught exception while creating ValueAggregator for 
aggregation function: " + aggregationFunction, e);
-          }
-          
Preconditions.checkState(valueAggregator.isAggregatedValueFixedSize(),
-              "Aggregation function: %s must have fixed size aggregated 
value", aggregationFunction);
-
-          aggregationSourceColumns.add(firstArgument.getIdentifier());
+    // Filter config
+    FilterConfig filterConfig = ingestionConfig.getFilterConfig();
+    if (filterConfig != null) {
+      String filterFunction = filterConfig.getFilterFunction();
+      if (filterFunction != null) {
+        if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(filterFunction)) {
+          throw new IllegalStateException(
+              "Groovy filter functions are disabled for table config. Found '" 
+ filterFunction + "'");
         }
-        Preconditions.checkState(new 
HashSet<>(schema.getMetricNames()).equals(aggregationColumns),
-            "all metric columns must be aggregated");
-
-        // This is required by 
MutableSegmentImpl.enableMetricsAggregationIfPossible().
-        // That code will disable ingestion aggregation if all metrics aren't 
noDictionaryColumns.
-        // But if you do that after the table is already created, all future 
aggregations will
-        // just be the default value.
-        Map<String, DictionaryIndexConfig> configPerCol = 
StandardIndexes.dictionary().getConfig(tableConfig, schema);
-        aggregationColumns.forEach(column -> {
-          DictionaryIndexConfig dictConfig = configPerCol.get(column);
-          Preconditions.checkState(dictConfig != null && 
dictConfig.isDisabled(),
-              "Aggregated column: %s must be a no-dictionary column", column);
-        });
-      }
-
-      // Source field configs
-      List<SourceFieldConfig> sourceFieldConfigs = 
ingestionConfig.getSourceFieldConfigs();
-      if (sourceFieldConfigs != null) {
-        // A source field can be configured once per phase (pre- and 
post-complex-type), but not twice within the same
-        // phase, because that would yield two DataTypeTransformers targeting 
it in the same phase, which is ambiguous.
-        Set<String> preComplexTypeFieldNames = new HashSet<>();
-        Set<String> postComplexTypeFieldNames = new HashSet<>();
-        for (SourceFieldConfig sourceFieldConfig : sourceFieldConfigs) {
-          String name = sourceFieldConfig.getName();
-          boolean preComplexTypeTransform = 
sourceFieldConfig.isPreComplexTypeTransform();
-          Set<String> fieldNames = preComplexTypeTransform ? 
preComplexTypeFieldNames : postComplexTypeFieldNames;
-          Preconditions.checkState(fieldNames.add(name),
-              "Duplicate SourceFieldConfig found for source field: %s 
(preComplexTypeTransform: %s)", name,
-              preComplexTypeTransform);
+        try {
+          FunctionEvaluatorFactory.getExpressionEvaluator(filterFunction);
+        } catch (Exception e) {
+          throw new IllegalStateException(
+              "Invalid filter function '" + filterFunction + "', exception: " 
+ e.getMessage(), e);
         }
       }
+    }
 
-      // Enrichment configs
-      List<EnrichmentConfig> enrichmentConfigs = 
ingestionConfig.getEnrichmentConfigs();
-      if (enrichmentConfigs != null) {
-        for (EnrichmentConfig enrichmentConfig : enrichmentConfigs) {
-          RecordEnricherRegistry.validateEnrichmentConfig(enrichmentConfig,
-              new RecordEnricherValidationConfig(_disableGroovy));
+    // Source field configs
+    List<SourceFieldConfig> sourceFieldConfigs = 
ingestionConfig.getSourceFieldConfigs();
+    if (sourceFieldConfigs != null) {
+      // A source field can be configured once per phase (pre- and 
post-complex-type), but not twice within the same
+      // phase, because that would yield two DataTypeTransformers targeting it 
in the same phase, which is ambiguous.
+      Set<String> preComplexTypeFieldNames = new HashSet<>();
+      Set<String> postComplexTypeFieldNames = new HashSet<>();
+      for (SourceFieldConfig sourceFieldConfig : sourceFieldConfigs) {
+        String name = sourceFieldConfig.getName();
+        boolean preComplexTypeTransform = 
sourceFieldConfig.isPreComplexTypeTransform();
+        Set<String> fieldNames = preComplexTypeTransform ? 
preComplexTypeFieldNames : postComplexTypeFieldNames;
+        Preconditions.checkState(fieldNames.add(name),
+            "Duplicate SourceFieldConfig found for source field: %s 
(preComplexTypeTransform: %s)", name,
+            preComplexTypeTransform);
+      }
+    }
+
+    // Enrichment configs
+    List<EnrichmentConfig> enrichmentConfigs = 
ingestionConfig.getEnrichmentConfigs();
+    if (enrichmentConfigs != null) {
+      for (EnrichmentConfig enrichmentConfig : enrichmentConfigs) {
+        RecordEnricherRegistry.validateEnrichmentConfig(enrichmentConfig,
+            new RecordEnricherValidationConfig(_disableGroovy));
+      }
+    }
+
+    // Transform configs
+    List<TransformConfig> transformConfigs = 
ingestionConfig.getTransformConfigs();
+    if (transformConfigs != null) {
+      // Pre-pass: collect every column referenced as a transform-function 
argument. A transform whose destination
+      // is not in the schema is still valid when another transform consumes 
it as an input - i.e. it is an
+      // intermediate ("derived") column. This enables chained / parse-once 
transforms, e.g.
+      //   message_obj = jsonExtractObject(message)              // 
intermediate, not in the schema
+      //   level       = JSONPATHSTRING(message_obj, '$.level')  // consumes 
the intermediate
+      // The intermediate is materialized in the record during transformation 
and dropped before indexing (only
+      // schema columns are indexed). Unreferenced non-schema destinations 
still fail below (typo protection).
+      Set<String> transformInputColumns = new HashSet<>();
+      for (TransformConfig transformConfig : transformConfigs) {
+        String transformFunction = transformConfig.getTransformFunction();
+        // Skip Groovy expressions when Groovy is disabled: do not compile 
them just to collect arguments (the main
+        // loop below rejects Groovy without compiling). Such a config is 
rejected anyway, so these columns are not
+        // needed as valid intermediate targets.
+        if (transformFunction != null && !(_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(
+            transformFunction))) {
+          try {
+            transformInputColumns.addAll(
+                
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction).getArguments());
+          } catch (Exception ignore) {
+            // Invalid functions are reported with a descriptive error in the 
main loop below.
+          }
         }
       }
-
-      // Transform configs
-      List<TransformConfig> transformConfigs = 
ingestionConfig.getTransformConfigs();
-      if (transformConfigs != null) {
-        // Pre-pass: collect every column referenced as a transform-function 
argument. A transform whose destination
-        // is not in the schema is still valid when another transform consumes 
it as an input - i.e. it is an
-        // intermediate ("derived") column. This enables chained / parse-once 
transforms, e.g.
-        //   message_obj = jsonExtractObject(message)              // 
intermediate, not in the schema
-        //   level       = JSONPATHSTRING(message_obj, '$.level')  // consumes 
the intermediate
-        // The intermediate is materialized in the record during 
transformation and dropped before indexing (only
-        // schema columns are indexed). Unreferenced non-schema destinations 
still fail below (typo protection).
-        Set<String> transformInputColumns = new HashSet<>();
-        for (TransformConfig transformConfig : transformConfigs) {
-          String transformFunction = transformConfig.getTransformFunction();
-          // Skip Groovy expressions when Groovy is disabled: do not compile 
them just to collect arguments (the main
-          // loop below rejects Groovy without compiling). Such a config is 
rejected anyway, so these columns are not
-          // needed as valid intermediate targets.
-          if (transformFunction != null
-              && !(_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(transformFunction))) {
-            try {
-              transformInputColumns.addAll(
-                  
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction).getArguments());
-            } catch (Exception ignore) {
-              // Invalid functions are reported with a descriptive error in 
the main loop below.
-            }
-          }
+      Set<String> transformColumns = new HashSet<>();
+      for (TransformConfig transformConfig : transformConfigs) {
+        String columnName = transformConfig.getColumnName();
+        String transformFunction = transformConfig.getTransformFunction();
+        if (columnName == null || transformFunction == null) {
+          throw new IllegalStateException(
+              "columnName/transformFunction cannot be null in TransformConfig 
" + transformConfig);
         }
-        Set<String> transformColumns = new HashSet<>();
-        for (TransformConfig transformConfig : transformConfigs) {
-          String columnName = transformConfig.getColumnName();
-          String transformFunction = transformConfig.getTransformFunction();
-          if (columnName == null || transformFunction == null) {
-            throw new IllegalStateException(
-                "columnName/transformFunction cannot be null in 
TransformConfig " + transformConfig);
-          }
-          if (!transformColumns.add(columnName)) {
-            throw new IllegalStateException("Duplicate transform config found 
for column '" + columnName + "'");
-          }
-          Preconditions.checkState(
-              schema.hasColumn(columnName) || 
aggregationSourceColumns.contains(columnName)
-                  || transformInputColumns.contains(columnName),
-              "The destination column '" + columnName
-                  + "' of the transform function must be present in the 
schema, be consumed as the input of another "
-                  + "transform function, or be a source column for 
aggregations");
-          FunctionEvaluator expressionEvaluator;
-          if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(transformFunction)) {
-            throw new IllegalStateException(
-                "Groovy transform functions are disabled for table config. 
Found '" + transformFunction
-                    + "' for column '" + columnName + "'");
-          }
-          try {
-            expressionEvaluator = 
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction);
-          } catch (Exception e) {
-            throw new IllegalStateException(
-                "Invalid transform function '" + transformFunction + "' for 
column '" + columnName + "', exception: "
-                    + e.getMessage(), e);
-          }
-          List<String> arguments = expressionEvaluator.getArguments();
-          if (arguments.contains(columnName)) {
-            throw new IllegalStateException(
-                "Arguments of a transform function '" + arguments + "' cannot 
contain the destination column '"
-                    + columnName + "'");
-          }
+        if (!transformColumns.add(columnName)) {
+          throw new IllegalStateException("Duplicate transform config found 
for column '" + columnName + "'");
+        }
+        Preconditions.checkState(schema.hasColumn(columnName) || 
aggregationSourceColumns.contains(columnName)
+            || transformInputColumns.contains(columnName), "The destination 
column '" + columnName
+            + "' of the transform function must be present in the schema, be 
consumed as the input of another "
+            + "transform function, or be a source column for aggregations");
+        FunctionEvaluator expressionEvaluator;
+        if (_disableGroovy && 
FunctionEvaluatorFactory.isGroovyExpression(transformFunction)) {
+          throw new IllegalStateException(
+              "Groovy transform functions are disabled for table config. Found 
'" + transformFunction + "' for column '"
+                  + columnName + "'");
+        }
+        try {
+          expressionEvaluator = 
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction);
+        } catch (Exception e) {
+          throw new IllegalStateException(
+              "Invalid transform function '" + transformFunction + "' for 
column '" + columnName + "', exception: "
+                  + e.getMessage(), e);
+        }
+        List<String> arguments = expressionEvaluator.getArguments();
+        if (arguments.contains(columnName)) {
+          throw new IllegalStateException(
+              "Arguments of a transform function '" + arguments + "' cannot 
contain the destination column '"
+                  + columnName + "'");
         }
       }
+    }
 
-      // Complex configs
-      ComplexTypeConfig complexTypeConfig = 
ingestionConfig.getComplexTypeConfig();
-      if (complexTypeConfig != null) {
-        Map<String, String> prefixesToRename = 
complexTypeConfig.getPrefixesToRename();
-        if (MapUtils.isNotEmpty(prefixesToRename)) {
-          Set<String> fieldNames = schema.getColumnNames();
-          for (String prefix : prefixesToRename.keySet()) {
-            for (String field : fieldNames) {
-              Preconditions.checkState(!field.startsWith(prefix),
-                  "Fields in the schema may not begin with any prefix 
specified in the prefixesToRename"
-                      + " config. Name conflict with field: " + field + " and 
prefix: " + prefix);
-            }
+    // Complex configs
+    ComplexTypeConfig complexTypeConfig = 
ingestionConfig.getComplexTypeConfig();
+    if (complexTypeConfig != null) {
+      Map<String, String> prefixesToRename = 
complexTypeConfig.getPrefixesToRename();
+      if (MapUtils.isNotEmpty(prefixesToRename)) {
+        Set<String> fieldNames = schema.getColumnNames();
+        for (String prefix : prefixesToRename.keySet()) {
+          for (String field : fieldNames) {
+            Preconditions.checkState(!field.startsWith(prefix),
+                "Fields in the schema may not begin with any prefix specified 
in the prefixesToRename"
+                    + " config. Name conflict with field: " + field + " and 
prefix: " + prefix);
           }
         }
       }
+    }
+
+    SchemaConformingTransformerConfig schemaConformingTransformerConfig =
+        ingestionConfig.getSchemaConformingTransformerConfig();
+    if (schemaConformingTransformerConfig != null) {
+      SchemaConformingTransformer.validateSchema(schema, 
schemaConformingTransformerConfig);
+    }
+  }
 
-      SchemaConformingTransformerConfig schemaConformingTransformerConfig =
-          ingestionConfig.getSchemaConformingTransformerConfig();
-      if (schemaConformingTransformerConfig != null) {
-        SchemaConformingTransformer.validateSchema(schema, 
schemaConformingTransformerConfig);
+  /// Validates all metrics-aggregation configuration for both mechanisms (the 
`aggregateMetrics` flag and ingestion
+  /// `aggregationConfigs`) in one place, and returns the set of source 
columns referenced by the aggregation functions
+  /// (empty when aggregation is disabled). Consuming-segment rollup keys each 
row on the dictionary ids of the
+  /// dimension and time columns and mutates the aggregated value in place in 
the metric's raw forward index, so:
+  /// - The two mechanisms are mutually exclusive, and aggregation is 
incompatible with upsert / dedup.
+  /// - The schema must not contain a COMPLEX column (neither a valid key nor 
an aggregatable metric).
+  /// - Every dimension column must be single-valued; the rollup key holds a 
single dictionary id per dimension, so it
+  ///   cannot represent a row with multiple values for that column (issue 
#3867). No-dictionary dimensions are allowed
+  ///   — the consuming segment force-creates a dictionary for them.
+  /// - Every metric column must be single-valued and no-dictionary 
(aggregated values are mutated in place; a
+  ///   dictionary-encoded metric would silently disable aggregation once the 
table is created).
+  /// - For `aggregationConfigs`, every metric must be covered by a valid 
aggregation function whose aggregated value
+  ///   is fixed size.
+  private static Set<String> validateMetricsAggregation(TableConfig 
tableConfig, Schema schema) {
+    IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+    boolean aggregateMetrics = 
tableConfig.getIndexingConfig().isAggregateMetrics();
+    List<AggregationConfig> aggregationConfigs =
+        ingestionConfig != null ? ingestionConfig.getAggregationConfigs() : 
null;

Review Comment:
   validateMetricsAggregation() dereferences tableConfig.getIndexingConfig() 
without a null check. If a table config is missing tableIndexConfig (common 
validation failure case), this will throw a NullPointerException instead of a 
clear validation error, and this method now runs even when ingestionConfig is 
null.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to