Vamsi-klu commented on code in PR #19087:
URL: https://github.com/apache/pinot/pull/19087#discussion_r3994755000


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/RecordTransformerUtils.java:
##########
@@ -142,29 +154,193 @@ private static void addIfNotNoOp(List<RecordTransformer> 
transformers, @Nullable
     }
   }
 
-  private static void addSourceFieldDataTypeTransformer(TableConfig 
tableConfig, List<RecordTransformer> transformers,
-      boolean preComplexTypeTransform) {
+  private static void addSourceFieldDataTypeTransformer(TableConfig 
tableConfig, @Nullable Schema schema,
+      List<RecordTransformer> transformers, boolean preComplexTypeTransform) {
     IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
     if (ingestionConfig == null) {
       return;
     }
-    List<SourceFieldConfig> sourceFieldConfigs = 
ingestionConfig.getSourceFieldConfigs();
-    if (CollectionUtils.isEmpty(sourceFieldConfigs)) {
-      return;
-    }
     Map<String, PinotDataType> dataTypes = new HashMap<>();
-    for (SourceFieldConfig sourceFieldConfig : sourceFieldConfigs) {
-      // If pre-ComplexType transformers are requested, add only 
pre-ComplexType source fields. Similarly, if
-      // non pre-ComplexType transformers are requested, add only non 
pre-ComplexType source fields.
-      if (sourceFieldConfig.isPreComplexTypeTransform() == 
preComplexTypeTransform) {
-        dataTypes.put(sourceFieldConfig.getName(), 
sourceFieldConfig.getDataType());
+    List<SourceFieldConfig> sourceFieldConfigs = 
ingestionConfig.getSourceFieldConfigs();
+    if (CollectionUtils.isNotEmpty(sourceFieldConfigs)) {
+      for (SourceFieldConfig sourceFieldConfig : sourceFieldConfigs) {
+        // If pre-ComplexType transformers are requested, add only 
pre-ComplexType source fields. Similarly, if
+        // non pre-ComplexType transformers are requested, add only non 
pre-ComplexType source fields.
+        if (sourceFieldConfig.isPreComplexTypeTransform() == 
preComplexTypeTransform) {
+          dataTypes.put(sourceFieldConfig.getName(), 
sourceFieldConfig.getDataType());
+        }
       }
     }
+    // Opt-in: convert aggregation source columns that are not in the schema 
(and not already covered by an explicit
+    // SourceFieldConfig) so mistyped JSON/Avro string numbers are converted 
before MutableSegmentImpl indexes them.
+    // Off by default; uses the stock DataTypeTransformer (no lazy 
compatibility short-circuit).
+    if (!preComplexTypeTransform && schema != null && 
ingestionConfig.isConvertAggregationSourceTypes()) {
+      addAggregationSourceDataTypes(tableConfig, schema, dataTypes);
+    }
     if (!dataTypes.isEmpty()) {
       transformers.add(new DataTypeTransformer(tableConfig, dataTypes));
     }
   }
 
+  /// Derives [PinotDataType]s for ingestion-aggregation source columns that 
are absent from the schema (and not already
+  /// covered by an explicit [SourceFieldConfig] in either phase). Types are 
inferred from the aggregation function and
+  /// destination metric. When one source feeds multiple aggregations, 
inferred numeric types are merged by keeping the
+  /// wider type so config order cannot drop precision. Sketch/HLL/COUNT 
sources are left unconverted so offering
+  /// semantics (e.g. hashing a string vs a number) are preserved.
+  /// 
[org.apache.pinot.segment.local.aggregator.ValueAggregatorUtils#toDouble] 
remains a safety net.
+  @VisibleForTesting
+  static void addAggregationSourceDataTypes(TableConfig tableConfig, Schema 
schema,
+      Map<String, PinotDataType> dataTypes) {
+    IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+    List<AggregationConfig> aggregationConfigs = 
ingestionConfig.getAggregationConfigs();
+    if (CollectionUtils.isEmpty(aggregationConfigs)) {
+      return;
+    }
+    // dataTypes only has this phase's SourceFieldConfigs. Pre-complex-type 
names are absent from the post-phase map
+    // and must still skip inference so an explicit type is not overwritten.
+    Set<String> explicitSourceFields = 
getExplicitSourceFieldNames(ingestionConfig);
+    for (AggregationConfig aggregationConfig : aggregationConfigs) {
+      String destColumn = aggregationConfig.getColumnName();
+      String aggregationFunction = aggregationConfig.getAggregationFunction();
+      ExpressionContext expressionContext;
+      try {
+        expressionContext = 
RequestContextUtils.getExpression(aggregationFunction);
+      } catch (Exception e) {
+        // Invalid configs are rejected at table-create validation time; skip 
here to keep transformer build resilient.
+        continue;
+      }
+      if (expressionContext.getType() != ExpressionContext.Type.FUNCTION) {
+        continue;
+      }
+      FunctionContext functionContext = expressionContext.getFunction();
+      AggregationFunctionType functionType;
+      try {
+        functionType = 
AggregationFunctionType.getAggregationFunctionType(functionContext.getFunctionName());
+      } catch (Exception e) {
+        continue;
+      }
+      List<ExpressionContext> arguments = functionContext.getArguments();
+      if (arguments.isEmpty()) {
+        continue;
+      }
+      ExpressionContext firstArgument = arguments.get(0);
+      if (firstArgument.getType() != ExpressionContext.Type.IDENTIFIER) {
+        continue;
+      }
+      String sourceColumn = firstArgument.getIdentifier();
+      if (AggregationFunctionColumnPair.STAR.equals(sourceColumn) || 
schema.hasColumn(sourceColumn)
+          || explicitSourceFields.contains(sourceColumn)) {
+        // Any explicit SourceFieldConfig (including pre-complex-type) or 
schema column already covers conversion;
+        // COUNT(*) has no source value.
+        continue;
+      }
+      FieldSpec destFieldSpec = schema.getFieldSpecFor(destColumn);
+      PinotDataType inferredType = 
inferAggregationSourceDataType(functionType, destFieldSpec);
+      if (inferredType != null) {
+        PinotDataType existing = dataTypes.get(sourceColumn);
+        dataTypes.put(sourceColumn,
+            existing == null ? inferredType : 
mergeInferredAggregationSourceTypes(existing, inferredType));

Review Comment:
   Thanks for the reproduction. Agreed this is a correctness bug, not an opt-in 
caveat.
   
   Skipping inference on DISTINCTCOUNTHLL only skips that one aggregation. 
SUM(metric) still registers metric to DOUBLE, and DataTypeTransformer rewrites 
the shared field before either aggregator runs. "01" and "1" both become 
Double(1.0), so HLL cardinality drops 2 to 1. Config order does not matter.
   
   We should not infer a source type from one aggregation and rewrite the 
field. Plan:
   1. Per-source compatibility: register a rewrite only if every aggregation 
that reads that source is conversion-safe. Any COUNT / HLL / sketch / bitmap 
consumer vetoes the rewrite.
   2. Keep convertAggregationSourceTypes (default false) and the stock 
DataTypeTransformer for numeric-only sources.
   3. Leave explicit sourceFieldConfigs as the override, and document that they 
are unsafe when an identity-sensitive aggregation shares the column.
   4. Add a TransformPipeline + MutableSegmentImpl regression for SUM(metric) + 
DISTINCTCOUNTHLL(metric, 12) with "01" and "1". Flag on and off both expect 
SUM=2.0 and HLL cardinality=2.
   
   If we also need fail-before-mutate for mixed tables, that would be a 
follow-up: convert a local copy at the numeric aggregator input and never 
putValue on the shared field. I would rather not move that into 
MutableSegmentImpl unless you want it in this PR.
   
   Does the compatibility veto match what you want?



-- 
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