xiangfu0 opened a new pull request, #19244:
URL: https://github.com/apache/pinot/pull/19244

   ## Summary
   
   Allow table-level ingestion transforms on `OFFLINE` tables to use registered 
scalar functions from any volatility category. New or changed `REALTIME` table 
ingestion transforms continue to require `IMMUTABLE` functions.
   
   The rest of transform validation remains in place, including expression 
parsing, destination-column validation, self-reference checks, and the Groovy 
policy. Schema-level transforms and `postPartialUpsertTransformConfigs` also 
retain their existing volatility checks.
   
   ## Motivation
   
   An offline upsert table needs a comparison value to choose the latest row 
for each primary key. Daily Parquet inputs can contain only a subset of the key 
space and may not contain a physical timestamp or row-version column. A value 
that is fixed for every ingestion run cannot order updates across those daily 
drops.
   
   This change lets an offline table declare a generated comparison column in 
the Pinot schema and populate it with `now()` or another explicitly selected 
non-immutable transform, without first rewriting the Parquet files. The policy 
change is scoped to finite, task-driven `OFFLINE` ingestion and is not applied 
to `REALTIME` ingestion.
   
   ## Usage
   
   Declare the generated comparison column in the Pinot schema even though it 
is absent from the source Parquet file:
   
   ```json
   {
     "schemaName": "dailyDimension",
     "dimensionFieldSpecs": [
       {
         "name": "id",
         "dataType": "STRING"
       }
     ],
     "metricFieldSpecs": [
       {
         "name": "ingestionTime",
         "dataType": "LONG"
       }
     ],
     "primaryKeyColumns": ["id"]
   }
   ```
   
   Configure full upsert and populate the comparison column during offline 
ingestion:
   
   ```json
   {
     "tableName": "dailyDimension",
     "tableType": "OFFLINE",
     "segmentsConfig": {
       "segmentPushType": "APPEND",
       "replication": "1"
     },
     "routing": {
       "instanceSelectorType": "strictReplicaGroup"
     },
     "tableIndexConfig": {
       "segmentPartitionConfig": {
         "columnPartitionMap": {
           "id": {
             "functionName": "Murmur",
             "numPartitions": 1
           }
         }
       }
     },
     "upsertConfig": {
       "mode": "FULL",
       "comparisonColumns": ["ingestionTime"]
     },
     "ingestionConfig": {
       "transformConfigs": [
         {
           "columnName": "ingestionTime",
           "transformFunction": "now()"
         }
       ]
     }
   }
   ```
   
   The Parquet rows must contain `id`, but they do not need to contain 
`ingestionTime`. Offline upsert still requires primary-key partitioning and 
strict-replica-group routing.
   
   ## Compatibility
   
   - `OFFLINE` table-level ingestion transforms accept `IMMUTABLE`, `STABLE`, 
and `VOLATILE` functions.
   - New or changed `REALTIME` table-level ingestion transforms continue to 
reject non-immutable functions.
   - An identical non-immutable transform already stored on a `REALTIME` table 
remains grandfathered so unrelated config updates are not blocked.
   - Schema-level and post-partial-upsert transforms continue to require 
immutable functions.
   - Unknown functions, invalid expressions, and disabled Groovy transforms 
remain rejected.
   
   ## Validation
   
   - `TableConfigUtilsTest`: 78 tests passed.
   - `SchemaUtilsTest`: 15 tests passed.
   - `PinotTableRestletResourceTest` and `TableConfigsRestletResourceTest`: 45 
tests passed.
   - Spotless, Checkstyle, license formatting/checking, and `git diff --check` 
passed for all affected modules.
   
   ## Draft status and known correctness gap
   
   This PR changes validation policy only; it does not make volatile functions 
batch-stable. Row-based offline segment generation currently evaluates 
transforms once while gathering statistics and again while indexing after 
rewinding the input. A function such as `now()` or `rand()` can therefore 
produce different values between the two passes. Dictionary-encoded columns can 
resolve a second-pass value that was absent from the first-pass dictionary to 
an incorrect dictionary ID, while no-dictionary columns can store values 
inconsistent with first-pass min/max metadata. Either case can select the wrong 
offline-upsert winner.
   
   Values can also differ across records, retries, rebuilds, and backfills, and 
registered functions that depend on unavailable runtime context can validate 
but fail during batch evaluation.
   
   This should remain a draft discussion PR until segment generation either 
materializes and reuses transformed rows or provides a defined build-stable 
evaluation mode, with an end-to-end segment-generation/readback regression test.
   


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