Copilot commented on code in PR #19087:
URL: https://github.com/apache/pinot/pull/19087#discussion_r3762499762
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/RecordTransformerUtils.java:
##########
@@ -142,26 +152,142 @@ 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) {
+ Map<String, PinotDataType> dataTypes = new HashMap<>();
+ IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+ if (ingestionConfig != null) {
+ 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());
+ }
+ }
+ }
+ }
+ // Auto-register aggregation source columns not in the schema so mistyped
JSON/Avro string numbers are converted
+ // before MutableSegmentImpl indexes them. Explicit SourceFieldConfig wins
(already in dataTypes). Only runs in the
+ // post-complex-type phase so flattened/unnested fields are available.
Auto-derived columns are converted lazily
+ // (only when the incoming value is incompatible with the aggregators) to
avoid per-record conversion overhead on
+ // the common correctly-typed path.
+ Set<String> lazyColumns = new HashSet<>();
+ if (!preComplexTypeTransform && schema != null) {
+ addAggregationSourceDataTypes(tableConfig, schema, dataTypes,
lazyColumns);
+ }
+ if (!dataTypes.isEmpty()) {
+ transformers.add(new DataTypeTransformer(tableConfig, dataTypes,
lazyColumns));
+ }
+ }
+
+ /// Derives [PinotDataType]s for ingestion-aggregation source columns that
are absent from the schema (and not already
+ /// covered by an explicit [SourceFieldConfig]). Types are inferred from the
aggregation function and destination
+ /// metric, and recorded in `lazyColumns` so that [DataTypeTransformer] only
converts values the aggregators cannot
+ /// consume directly. 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.
+ static void addAggregationSourceDataTypes(TableConfig tableConfig, Schema
schema,
+ Map<String, PinotDataType> dataTypes, Set<String> lazyColumns) {
IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
if (ingestionConfig == null) {
return;
}
- List<SourceFieldConfig> sourceFieldConfigs =
ingestionConfig.getSourceFieldConfigs();
- if (CollectionUtils.isEmpty(sourceFieldConfigs)) {
+ List<AggregationConfig> aggregationConfigs =
ingestionConfig.getAggregationConfigs();
+ if (CollectionUtils.isEmpty(aggregationConfigs)) {
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());
+ for (AggregationConfig aggregationConfig : aggregationConfigs) {
+ String destColumn = aggregationConfig.getColumnName();
+ String aggregationFunction = aggregationConfig.getAggregationFunction();
+ if (destColumn == null || aggregationFunction == null) {
+ continue;
+ }
+ 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)
+ || dataTypes.containsKey(sourceColumn)) {
Review Comment:
An explicit `SourceFieldConfig` with `preComplexTypeTransform=true` is not
present in this post-phase `dataTypes` map, so the same source is
auto-registered again and its explicitly selected type can be overwritten after
complex transformation. Check all configured source fields here, not only those
selected for this phase, so the documented “explicit config wins” rule holds.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/DataTypeTransformer.java:
##########
@@ -144,6 +161,17 @@ public void transform(GenericRow record) {
}
}
+ /// Returns true when a lazily-converted aggregation source value can be
consumed directly by the
+ /// [org.apache.pinot.segment.local.aggregator.ValueAggregator]s without
conversion: null, any [Number] box
+ /// (`ValueAggregatorUtils.toDouble` and `SumPrecisionValueAggregator`
accept them), or a multi-value array that is
+ /// already of the target array type.
+ private static boolean isAggregatorCompatible(@Nullable Object value,
PinotDataType targetType) {
+ if (value == null || value instanceof Number) {
+ return true;
+ }
+ return targetType == PinotDataType.DOUBLE_ARRAY && value instanceof
Double[];
Review Comment:
This compatibility check excludes input forms that the existing aggregators
deliberately consume. For example,
`AvgValueAggregator`/`MinMaxRangeValueAggregator` merge serialized `byte[]`,
while SUM/MIN/MAX/AVG and related aggregators accept multi-element `Object[]`;
these now enter scalar conversion and fail (`BYTES`→`DOUBLE` or array→single
value) before indexing. Preserve aggregator-specific accepted forms or make
conversion shape-aware.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/recordtransformer/RecordTransformerUtils.java:
##########
@@ -142,26 +152,142 @@ 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) {
+ Map<String, PinotDataType> dataTypes = new HashMap<>();
+ IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
+ if (ingestionConfig != null) {
+ 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());
+ }
+ }
+ }
+ }
+ // Auto-register aggregation source columns not in the schema so mistyped
JSON/Avro string numbers are converted
+ // before MutableSegmentImpl indexes them. Explicit SourceFieldConfig wins
(already in dataTypes). Only runs in the
+ // post-complex-type phase so flattened/unnested fields are available.
Auto-derived columns are converted lazily
+ // (only when the incoming value is incompatible with the aggregators) to
avoid per-record conversion overhead on
+ // the common correctly-typed path.
+ Set<String> lazyColumns = new HashSet<>();
+ if (!preComplexTypeTransform && schema != null) {
+ addAggregationSourceDataTypes(tableConfig, schema, dataTypes,
lazyColumns);
+ }
+ if (!dataTypes.isEmpty()) {
+ transformers.add(new DataTypeTransformer(tableConfig, dataTypes,
lazyColumns));
+ }
+ }
+
+ /// Derives [PinotDataType]s for ingestion-aggregation source columns that
are absent from the schema (and not already
+ /// covered by an explicit [SourceFieldConfig]). Types are inferred from the
aggregation function and destination
+ /// metric, and recorded in `lazyColumns` so that [DataTypeTransformer] only
converts values the aggregators cannot
+ /// consume directly. 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.
+ static void addAggregationSourceDataTypes(TableConfig tableConfig, Schema
schema,
+ Map<String, PinotDataType> dataTypes, Set<String> lazyColumns) {
IngestionConfig ingestionConfig = tableConfig.getIngestionConfig();
if (ingestionConfig == null) {
return;
}
- List<SourceFieldConfig> sourceFieldConfigs =
ingestionConfig.getSourceFieldConfigs();
- if (CollectionUtils.isEmpty(sourceFieldConfigs)) {
+ List<AggregationConfig> aggregationConfigs =
ingestionConfig.getAggregationConfigs();
+ if (CollectionUtils.isEmpty(aggregationConfigs)) {
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());
+ for (AggregationConfig aggregationConfig : aggregationConfigs) {
+ String destColumn = aggregationConfig.getColumnName();
+ String aggregationFunction = aggregationConfig.getAggregationFunction();
+ if (destColumn == null || aggregationFunction == null) {
+ continue;
+ }
+ 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)
+ || dataTypes.containsKey(sourceColumn)) {
+ // Explicit SourceFieldConfig 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) {
+ dataTypes.put(sourceColumn, inferredType);
Review Comment:
This overwrite makes conversion depend on aggregation-config order when one
raw source feeds multiple destination metrics. For example, a valid
`SUM_PRECISION(metric, 38)` plus `AVG(metric)` configuration can let the later
`DOUBLE` inference replace `BIG_DECIMAL`, causing a large decimal string to
lose digits before `SumPrecisionValueAggregator` sees it. Merge compatible
requirements deterministically or reject conflicting inferred types.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/recordtransformer/RecordTransformerTest.java:
##########
@@ -615,6 +616,186 @@ public void testSourceFieldDataTypeConversion() {
assertNull(nullRecord.getValue("srcLong"));
}
+ @Test
+ public void testAggregationSourceAutoDataTypeConversion() {
+ // Aggregation source "metric" is not in the schema; TransformPipeline
should still convert string "123" before
+ // indexing (issue #16317).
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("aggSrcSchema")
+ .addSingleValueDimension("dim", DataType.STRING)
+ .addMetric("sumMetric", DataType.DOUBLE)
+ .addMetric("minMetric", DataType.DOUBLE)
+ .addMetric("maxMetric", DataType.DOUBLE)
+ .addDateTime("ts", DataType.LONG, "1:MILLISECONDS:EPOCH",
"1:MILLISECONDS")
+ .build();
+ IngestionConfig ingestionConfig = new IngestionConfig();
+ ingestionConfig.setAggregationConfigs(List.of(
+ new AggregationConfig("sumMetric", "SUM(metric)"),
+ new AggregationConfig("minMetric", "MIN(metric)"),
+ new AggregationConfig("maxMetric", "MAX(metric)")));
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("aggSrcTable")
+ .setTimeColumnName("ts")
+ .setNoDictionaryColumns(List.of("sumMetric", "minMetric", "maxMetric"))
+ .setIngestionConfig(ingestionConfig)
+ .build();
+
+ List<RecordTransformer> transformers =
RecordTransformerUtils.getDefaultTransformers(tableConfig, schema);
+ // First transformer is the post-complex-type source-field
DataTypeTransformer covering "metric".
+ assertTrue(transformers.get(0) instanceof DataTypeTransformer);
+ assertEquals(transformers.get(0).getInputColumns(), Set.of("metric"));
+
+ TransformPipeline pipeline = new TransformPipeline(tableConfig, schema);
+ GenericRow row = new GenericRow();
+ row.putValue("dim", "a");
+ row.putValue("ts", 1L);
+ row.putValue("metric", "123");
+ TransformPipeline.Result result = pipeline.processRow(row);
+ assertEquals(result.getTransformedRows().size(), 1);
+ assertEquals(result.getTransformedRows().get(0).getValue("metric"), 123.0);
+
+ // Lazy conversion: an already-typed value passes through as the same
object reference (no conversion, no
+ // allocation).
+ Double typedValue = 42.5;
+ GenericRow typedRow = new GenericRow();
+ typedRow.putValue("dim", "a");
+ typedRow.putValue("ts", 1L);
+ typedRow.putValue("metric", typedValue);
+
assertSame(pipeline.processRow(typedRow).getTransformedRows().get(0).getValue("metric"),
typedValue);
+
+ // Lazy conversion: a compatible Number of a different box (Integer for
DOUBLE target) is also passed through
+ // untouched; ValueAggregatorUtils.toDouble accepts any Number.
+ Integer intBoxValue = 7;
+ GenericRow intBoxRow = new GenericRow();
+ intBoxRow.putValue("dim", "a");
+ intBoxRow.putValue("ts", 1L);
+ intBoxRow.putValue("metric", intBoxValue);
+
assertSame(pipeline.processRow(intBoxRow).getTransformedRows().get(0).getValue("metric"),
intBoxValue);
+
+ // Non-numeric string fails before indexing when continueOnError is false.
+ GenericRow bad = new GenericRow();
+ bad.putValue("dim", "a");
+ bad.putValue("ts", 1L);
+ bad.putValue("metric", "abc");
+ try {
+ pipeline.processRow(bad);
+ fail("Expected data type conversion failure for non-numeric aggregation
source");
+ } catch (Exception e) {
+ // expected
+ }
+
+ // With continueOnError, unparsable source becomes null and the row is
marked incomplete.
+ ingestionConfig.setContinueOnError(true);
+ TransformPipeline continuePipeline = new TransformPipeline(tableConfig,
schema);
+ GenericRow badContinue = new GenericRow();
+ badContinue.putValue("dim", "a");
+ badContinue.putValue("ts", 1L);
+ badContinue.putValue("metric", "abc");
+ TransformPipeline.Result continueResult =
continuePipeline.processRow(badContinue);
+ assertEquals(continueResult.getTransformedRows().size(), 1);
+ assertNull(continueResult.getTransformedRows().get(0).getValue("metric"));
+ assertTrue(continueResult.getTransformedRows().get(0).isIncomplete());
+ assertEquals(continueResult.getIncompleteRowCount(), 1);
+ }
+
+ @Test
+ public void testAggregationSourceMultiValueAutoDataTypeConversion() {
+ // SUMMV source is multi-value: string elements must convert to Double[]
(not throw on multi-element arrays), and
+ // an already-typed Double[] must pass through as the same object
reference.
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("aggMvSrcSchema")
+ .addSingleValueDimension("dim", DataType.STRING)
+ .addMetric("summvMetric", DataType.DOUBLE)
+ .addDateTime("ts", DataType.LONG, "1:MILLISECONDS:EPOCH",
"1:MILLISECONDS")
+ .build();
+ IngestionConfig ingestionConfig = new IngestionConfig();
+ ingestionConfig.setAggregationConfigs(List.of(new
AggregationConfig("summvMetric", "SUMMV(metricMv)")));
+ TableConfig tableConfig = new
TableConfigBuilder(TableType.REALTIME).setTableName("aggMvSrcTable")
+ .setTimeColumnName("ts")
+ .setNoDictionaryColumns(List.of("summvMetric"))
+ .setIngestionConfig(ingestionConfig)
+ .build();
+
+ TransformPipeline pipeline = new TransformPipeline(tableConfig, schema);
+ GenericRow row = new GenericRow();
+ row.putValue("dim", "a");
+ row.putValue("ts", 1L);
+ row.putValue("metricMv", new Object[]{"1.5", "2.5"});
+ Object converted =
pipeline.processRow(row).getTransformedRows().get(0).getValue("metricMv");
+ assertEquals(converted, new Double[]{1.5, 2.5});
+
+ Double[] typedValues = new Double[]{3.5, 4.5};
+ GenericRow typedRow = new GenericRow();
+ typedRow.putValue("dim", "a");
+ typedRow.putValue("ts", 1L);
+ typedRow.putValue("metricMv", typedValues);
+
assertSame(pipeline.processRow(typedRow).getTransformedRows().get(0).getValue("metricMv"),
typedValues);
+
+ // Unparsable element fails in the transformer, before MutableSegmentImpl
mutates the row.
+ GenericRow bad = new GenericRow();
+ bad.putValue("dim", "a");
+ bad.putValue("ts", 1L);
+ bad.putValue("metricMv", new Object[]{"1.5", "abc"});
+ try {
+ pipeline.processRow(bad);
+ fail("Expected data type conversion failure for non-numeric multi-value
aggregation source");
+ } catch (Exception e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testAggregationSourceExplicitSourceFieldConfigWins() {
+ Schema schema = new Schema.SchemaBuilder().setSchemaName("aggSrcSchema")
+ .addSingleValueDimension("dim", DataType.STRING)
+ .addMetric("sumMetric", DataType.LONG)
+ .addDateTime("ts", DataType.LONG, "1:MILLISECONDS:EPOCH",
"1:MILLISECONDS")
+ .build();
+ IngestionConfig ingestionConfig = new IngestionConfig();
+ ingestionConfig.setAggregationConfigs(List.of(new
AggregationConfig("sumMetric", "SUM(metric)")));
+ // Explicit LONG conversion wins over auto DOUBLE inference from
destination LONG... destination is LONG so auto
+ // would also be LONG; use INT destination type via explicit override to
prove precedence.
Review Comment:
This explanation reverses the configured types: the
destination/auto-inferred type is `LONG`, while the explicit override is `INT`.
As written, the comment makes the precedence test misleading.
--
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]