Jackie-Jiang commented on code in PR #18977:
URL: https://github.com/apache/pinot/pull/18977#discussion_r4020495767


##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/creator/impl/BaseSegmentCreator.java:
##########
@@ -694,6 +700,52 @@ public static void 
addColumnMetadataInfo(PropertiesConfiguration properties, Str
     }
   }
 
+  /// Records a transform that actually produced the stored column values.
+  public static void addTransformFunction(PropertiesConfiguration properties, 
String column,
+      @Nullable String transformFunction) {
+    addTransformFunction(properties, column, transformFunction, false);
+  }
+
+  /// Records transform provenance for a column.
+  /// A real stored transform is written to 
[V1Constants.MetadataKeys.Column#TRANSFORM_FUNCTION]. A backward-compat
+  /// backfill (values were not regenerated from the expression) is written 
only to
+  /// [V1Constants.MetadataKeys.Column#TRANSFORM_FUNCTION_BACKFILLED], so the 
two cases stay distinguishable.
+  public static void addTransformFunction(PropertiesConfiguration properties, 
String column,
+      @Nullable String transformFunction, boolean backfilled) {
+    String transformFunctionKey = getKeyFor(column, TRANSFORM_FUNCTION);
+    String backfilledKey = getKeyFor(column, TRANSFORM_FUNCTION_BACKFILLED);
+    if (transformFunction == null) {
+      properties.clearProperty(transformFunctionKey);
+      properties.clearProperty(backfilledKey);
+      return;
+    }
+    String validTransformFunction = 
CommonsConfigurationUtils.replaceSpecialCharacterInPropertyValue(transformFunction);
+    if (validTransformFunction == null) {
+      throw new IllegalArgumentException("Cannot persist transform function 
for column: " + column
+          + " because it contains UTF-16 surrogate characters that segment 
metadata cannot store");

Review Comment:
   **Preserve valid Unicode transforms when storing provenance (new 
regression).**
   
   `replaceSpecialCharacterInPropertyValue()` rejects every UTF-16 surrogate, 
including the valid pair representing an emoji. An existing expression such as 
`concat(source, '😀')` now fails normal segment generation/sealing because 
`addColumnMetadataInfo()` calls this writer for ordinary ingested columns too. 
The failure is not limited to legacy BACKFILL.
   
   The expression can be represented reversibly; the current helper refuses to 
encode it. Consider a metadata encoding supporting full Unicode, with matching 
decoding and an encoding marker or separate key, so provenance is retained 
without rejecting previously valid transforms. A normal segment-generation 
round-trip test would cover the compatibility case beyond the current tests 
that expect BACKFILL to throw.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java:
##########
@@ -159,6 +161,8 @@ public void process(@Nullable SegmentOperationsThrottlerSet 
segmentOperationsThr
       DefaultColumnHandler defaultColumnHandler =
           DefaultColumnHandlerFactory.getDefaultColumnHandler(indexDir, 
segmentMetadata, _indexLoadingConfig,
               segmentWriter);
+      // Capture before apply: UPDATE_*_TRANSFORM_FUNCTION changes values; 
BACKFILL does not.
+      _columnsWithChangedTransformValues = 
defaultColumnHandler.getColumnsWithPendingTransformValueChanges();

Review Comment:
   **Also invalidate multi-column text indexes after transform value changes 
(existing gap, new trigger).**
   
   If a rewritten derived STRING column participates in a multi-column text 
index, `removeColumnIndices()` removes its individual indexes but leaves the 
shared `multi_col_text_idx` files and metadata. Later, 
`processMultiColTextIndex()` only compares index configuration and skips 
rebuilding when that configuration is unchanged. `TEXT_MATCH` therefore 
continues searching the old values while the forward index contains the new 
values.
   
   This gap already exists for schema/default-value updates; this PR makes it 
reachable through transform-only updates as well. Consider using the 
changed-column set to invalidate/rebuild the shared text index when it 
intersects the indexed columns, with a regression test that queries the updated 
text.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java:
##########
@@ -437,9 +465,11 @@ private boolean processStarTrees(File indexDir,
         StarTreeBuilderUtils.removeStarTrees(indexDir);
       } else {
         // NOTE: Always use OFF_HEAP mode on server side.
-        // Pass _indexLoadingConfig so downstream readers can resolve 
table-level configs we set
+        // Pass _indexLoadingConfig so downstream readers can resolve 
table-level configs we set.
+        // Force rebuild when transform values changed: reuse would keep stale 
aggregates because star-tree
+        // config is unchanged after UPDATE_*_TRANSFORM_FUNCTION.
         MultipleTreesBuilder builder = new 
MultipleTreesBuilder(starTreeBuilderConfigs, indexDir,
-            MultipleTreesBuilder.BuildMode.OFF_HEAP, _indexLoadingConfig);
+            MultipleTreesBuilder.BuildMode.OFF_HEAP, _indexLoadingConfig, 
transformValuesChangedOnStarTreeColumns);

Review Comment:
   **Avoid restoring an old tree after a value-changing rebuild fails (existing 
fallback, new failure scenario).**
   
   The transformed values and new expression metadata are saved before this 
forced rebuild. If `singleTreeBuilder.build()` fails, such as on an I/O error 
during generation, `MultipleTreesBuilder.close()` restores the previous tree 
and its metadata, while the catch below suppresses the build exception. 
Preprocessing can then succeed with new column values and old aggregates. The 
expression and tree configuration now match, so a subsequent reload also has no 
signal to retry.
   
   Restoring old trees is existing behavior, but the forced rebuild introduces 
this use after transform values have changed. Consider invalidating/removing 
the stale tree on this path or rolling back the complete segment state. An 
injected build-failure test after the column rewrite would cover it.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/defaultcolumn/BaseDefaultColumnHandler.java:
##########
@@ -298,20 +354,39 @@ Map<String, DefaultColumnAction> 
computeDefaultColumnActionMap() {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_METRIC_DEFAULT_VALUE);
           } else if (isSingleValueInMetadata != isSingleValueInSchema) {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_METRIC_NUMBER_OF_VALUES);
+          } else if (isTransformFunctionChanged(column, columnMetadata)) {
+            defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_METRIC_TRANSFORM_FUNCTION);
           }
         } else if (fieldTypeInMetadata == DATE_TIME) {
           if (dataTypeInMetadata != dataTypeInSchema) {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_DATE_TIME_DATA_TYPE);
           } else if (!defaultValueInSchema.equals(defaultValueInMetadata)) {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_DATE_TIME_DEFAULT_VALUE);
+          } else if (isTransformFunctionChanged(column, columnMetadata)) {
+            defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_DATE_TIME_TRANSFORM_FUNCTION);
           }
         } else if (fieldTypeInMetadata == COMPLEX) {
           if (dataTypeInMetadata != dataTypeInSchema) {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_COMPLEX_DATA_TYPE);
           } else if (!defaultValueInSchema.equals(defaultValueInMetadata)) {
             defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_COMPLEX_DEFAULT_VALUE);
+          } else if (isTransformFunctionChanged(column, columnMetadata)) {
+            defaultColumnActionMap.put(column, 
DefaultColumnAction.UPDATE_COMPLEX_TRANSFORM_FUNCTION);
           }
         }
+
+        // Segments created before the transform function was tracked in the 
metadata report null for both the stored
+        // and the backfilled field. Their values cannot be told apart from 
up-to-date ones, so instead of regenerating
+        // them, record the configured transform in 
TRANSFORM_FUNCTION_BACKFILLED (values untouched) so that the NEXT
+        // transform function change is detected. The expression is not 
written to TRANSFORM_FUNCTION, which is reserved
+        // for transforms that actually produced the stored values.
+        // Tradeoff: a transform function change that lands in the very same 
reload as this backfill is not applied to
+        // the existing values (which matches the behavior before the 
transform function was tracked at all); operators
+        // who need those values regenerated can force it with one more change 
to the expression.
+        if (!defaultColumnActionMap.containsKey(column) && 
getEffectiveTransformFunction(columnMetadata) == null
+            && getTransformFunctionForColumn(column) != null) {
+          defaultColumnActionMap.put(column, 
DefaultColumnAction.BACKFILL_TRANSFORM_FUNCTION);

Review Comment:
   **Distinguish known absence of a transform from legacy missing provenance 
(new remove/restore regression).**
   
   Starting with an auto-generated column produced by transform A: remove A and 
reload, then restore A and reload again. The first reload regenerates default 
values and clears both provenance fields. The second reload reaches this legacy 
BACKFILL branch, which records A without regenerating values. Further reloads 
are no-ops, leaving the defaults permanently despite restoring the original 
config.
   
   Before this PR, that remove/restore sequence left the original values 
untouched. Consider persisting a distinct known-no-transform state or metadata 
version so this transition regenerates values while genuinely legacy segments 
still receive metadata-only backfill. Extending 
`testRemovingTransformFunctionFromConfigRegeneratesDefaultValues` to restore 
the expression would cover the sequence.



##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/SegmentPreProcessor.java:
##########
@@ -409,15 +431,21 @@ private boolean processStarTrees(File indexDir,
 
     boolean shouldGenerateStarTree = !starTreeBuilderConfigs.isEmpty();
     boolean shouldRemoveStarTree = false;
+    boolean transformValuesChangedOnStarTreeColumns = false;
     List<StarTreeV2Metadata> starTreeMetadataList = 
segmentMetadata.getStarTreeV2MetadataList();
     if (starTreeMetadataList != null) {
       // There are existing star-trees
+      transformValuesChangedOnStarTreeColumns =
+          StarTreeBuilderUtils.usesAnyColumn(starTreeMetadataList, 
_columnsWithChangedTransformValues);

Review Comment:
   **Invalidate affected star-trees even when dynamic creation is disabled 
(existing gap, new trigger).**
   
   `processStarTrees()` returns at the `isEnableDynamicStarTreeCreation()` 
guard before reaching this check. With an existing tree over an auto-generated 
column—for example, one built while dynamic creation was enabled and retained 
after disabling it—the new transform-update path still rewrites the column 
first. The old tree remains queryable against the new column values.
   
   The early return and similar structural-update gap predate this PR. For the 
new transform path, invalidation needs to be independent of permission to 
create trees: remove affected trees, rebuild them, or prevent committing 
inconsistent state. The reload test could disable dynamic creation after the 
initial tree is built and verify that stale aggregates are never served.



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