This is an automated email from the ASF dual-hosted git repository.

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 2aa3aad815d Allow non-immutable ingestion transforms for offline 
tables (#19244)
2aa3aad815d is described below

commit 2aa3aad815d58114bb1761f4ab14fb1224830e0d
Author: Xiang Fu <[email protected]>
AuthorDate: Thu Aug 13 14:26:55 2026 -0700

    Allow non-immutable ingestion transforms for offline tables (#19244)
---
 .../api/PinotTableRestletResourceTest.java         | 13 ++++-----
 .../api/TableConfigsRestletResourceTest.java       | 20 ++++++-------
 .../apache/pinot/core/util/SchemaUtilsTest.java    | 13 ++++++---
 .../segment/local/utils/TableConfigUtils.java      |  7 +++--
 .../segment/local/utils/TableConfigUtilsTest.java  | 33 ++++++++++++----------
 5 files changed, 48 insertions(+), 38 deletions(-)

diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
index a6c9f8f1a36..4a4dfc54ce0 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
@@ -535,10 +535,9 @@ public class PinotTableRestletResourceTest extends 
ControllerTest {
       throws Exception {
     String tableName = "legacyNonDeterministicTransform";
     DEFAULT_INSTANCE.addDummySchema(tableName);
-
     IngestionConfig ingestionConfig = new IngestionConfig();
-    ingestionConfig.setTransformConfigs(List.of(new TransformConfig("dimA", 
"now()")));
-    TableConfig legacyTableConfig = getOfflineTableBuilder(tableName)
+    ingestionConfig.setTransformConfigs(List.of(new 
TransformConfig("timeColumn", "now()")));
+    TableConfig legacyTableConfig = getRealtimeTableBuilder(tableName)
         .setIngestionConfig(ingestionConfig)
         .build();
 
@@ -550,20 +549,20 @@ public class PinotTableRestletResourceTest extends 
ControllerTest {
     // Seed the config below the REST validation layer to model a table 
persisted before this validation existed.
     DEFAULT_INSTANCE.getHelixResourceManager().addTable(legacyTableConfig);
 
-    TableConfig update = getTableConfig(tableName, "OFFLINE");
+    TableConfig update = getTableConfig(tableName, "REALTIME");
     update.getValidationConfig().setRetentionTimeValue("10");
     JsonNode validationResponse = 
JsonUtils.stringToJsonNode(tableClient().validateTableConfig(update.toJsonString()));
-    assertTrue(validationResponse.has("OFFLINE"));
+    assertTrue(validationResponse.has("REALTIME"));
 
     JsonNode response = JsonUtils.stringToJsonNode(updateTable(tableName, 
update.toJsonString()));
     assertTrue(response.has("status"));
 
-    TableConfig stored = getTableConfig(tableName, "OFFLINE");
+    TableConfig stored = getTableConfig(tableName, "REALTIME");
     assertEquals(stored.getValidationConfig().getRetentionTimeValue(), "10");
     
assertEquals(stored.getIngestionConfig().getTransformConfigs().get(0).getTransformFunction(),
 "now()");
 
     IngestionConfig changedIngestionConfig = new IngestionConfig();
-    changedIngestionConfig.setTransformConfigs(List.of(new 
TransformConfig("dimA", "plus(now(), 1)")));
+    changedIngestionConfig.setTransformConfigs(List.of(new 
TransformConfig("timeColumn", "plus(now(), 1)")));
     update.setIngestionConfig(changedIngestionConfig);
     PinotAdminException validationError =
         expectThrows(PinotAdminException.class, () -> 
tableClient().validateTableConfig(update.toJsonString()));
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
index e7bbccbbcac..f3b332271c1 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
@@ -519,12 +519,12 @@ public class TableConfigsRestletResourceTest extends 
ControllerTest {
     Schema schema = createDummySchema(tableName);
 
     IngestionConfig ingestionConfig = new IngestionConfig();
-    ingestionConfig.setTransformConfigs(List.of(new TransformConfig("dimA", 
"now()")));
-    TableConfig legacyOfflineConfig = getBaseTableConfigBuilder(tableName, 
TableType.OFFLINE)
+    ingestionConfig.setTransformConfigs(List.of(new 
TransformConfig("timeColumn", "now()")));
+    TableConfig legacyRealtimeConfig = getBaseTableConfigBuilder(tableName, 
TableType.REALTIME)
         .setIngestionConfig(ingestionConfig)
         .build();
-    TableConfigs tableConfigs = new TableConfigs(tableName, schema, 
legacyOfflineConfig, null);
-    String tableNameWithType = 
TableNameBuilder.OFFLINE.tableNameWithType(tableName);
+    TableConfigs tableConfigs = new TableConfigs(tableName, schema, null, 
legacyRealtimeConfig);
+    String tableNameWithType = 
TableNameBuilder.REALTIME.tableNameWithType(tableName);
     try {
       String createError = Assert.expectThrows(Exception.class,
               () -> 
adminClient.getTableClient().createTableConfigs(tableConfigs.toPrettyJsonString(),
 null, null))
@@ -533,21 +533,21 @@ public class TableConfigsRestletResourceTest extends 
ControllerTest {
 
       // Seed the config below the REST validation layer to model a table 
persisted before this validation existed.
       DEFAULT_INSTANCE.addSchema(schema);
-      DEFAULT_INSTANCE.getHelixResourceManager().addTable(legacyOfflineConfig);
+      
DEFAULT_INSTANCE.getHelixResourceManager().addTable(legacyRealtimeConfig);
 
       TableConfigs update = 
adminClient.getTableClient().getTableConfigsObject(tableName);
-      update.getOffline().getValidationConfig().setRetentionTimeValue("10");
+      update.getRealtime().getValidationConfig().setRetentionTimeValue("10");
       adminClient.getTableClient()
           .updateTableConfigs(tableName, update.toPrettyJsonString(), null, 
false, false);
 
       TableConfigs stored = 
adminClient.getTableClient().getTableConfigsObject(tableName);
-      
Assert.assertEquals(stored.getOffline().getValidationConfig().getRetentionTimeValue(),
 "10");
-      
Assert.assertEquals(stored.getOffline().getIngestionConfig().getTransformConfigs().get(0).getTransformFunction(),
+      
Assert.assertEquals(stored.getRealtime().getValidationConfig().getRetentionTimeValue(),
 "10");
+      
Assert.assertEquals(stored.getRealtime().getIngestionConfig().getTransformConfigs().get(0).getTransformFunction(),
           "now()");
 
       IngestionConfig changedIngestionConfig = new IngestionConfig();
-      changedIngestionConfig.setTransformConfigs(List.of(new 
TransformConfig("dimA", "plus(now(), 1)")));
-      update.getOffline().setIngestionConfig(changedIngestionConfig);
+      changedIngestionConfig.setTransformConfigs(List.of(new 
TransformConfig("timeColumn", "plus(now(), 1)")));
+      update.getRealtime().setIngestionConfig(changedIngestionConfig);
       String updateError = Assert.expectThrows(Exception.class,
               () -> adminClient.getTableClient()
                   .updateTableConfigs(tableName, update.toPrettyJsonString(), 
null, false, false))
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java 
b/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
index 336f984ae5d..ad87a56909e 100644
--- a/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/core/util/SchemaUtilsTest.java
@@ -190,12 +190,17 @@ public class SchemaUtilsTest {
 
   @Test
   public void testCompatibilityGrandfathersExistingNonDeterministicTransform() 
{
-    Schema schema =
-        new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addMetric("eventTimeMs", 
DataType.LONG).build();
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addMetric("eventTimeMs", DataType.LONG)
+        .addDateTime(TIME_COLUMN, DataType.LONG, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
+        .build();
     IngestionConfig ingestionConfig = new IngestionConfig();
     ingestionConfig.setTransformConfigs(List.of(new 
TransformConfig("eventTimeMs", "now()")));
-    TableConfig tableConfig =
-        new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).setIngestionConfig(ingestionConfig).build();
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
+        .setStreamConfigs(getStreamConfigs())
+        .setIngestionConfig(ingestionConfig)
+        .build();
 
     // A new table using this transform is still rejected.
     Assert.expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validate(tableConfig, schema));
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
index fc2dcd1e84b..8ef3c48727e 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/TableConfigUtils.java
@@ -457,7 +457,8 @@ public final class TableConfigUtils {
   /// - 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.
+  ///   and does not reference its own destination. REALTIME tables require 
immutable functions, while OFFLINE tables
+  ///   allow all volatility categories.
   /// - Complex-type config: no schema field collides with a 
`prefixesToRename` prefix.
   /// - Schema-conforming transformer config.
   @VisibleForTesting
@@ -621,7 +622,9 @@ public final class TableConfigUtils {
                   + columnName + "'");
         }
         try {
-          validateIngestionTransformFunctionVolatility(transformConfig, 
existingTransformConfigs);
+          if (tableConfig.getTableType() == TableType.REALTIME) {
+            validateIngestionTransformFunctionVolatility(transformConfig, 
existingTransformConfigs);
+          }
           expressionEvaluator = 
FunctionEvaluatorFactory.getExpressionEvaluator(transformFunction);
         } catch (Exception e) {
           throw new IllegalStateException(
diff --git 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
index af4d7a85875..29ed034bcfa 100644
--- 
a/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
+++ 
b/pinot-segment-local/src/test/java/org/apache/pinot/segment/local/utils/TableConfigUtilsTest.java
@@ -381,7 +381,9 @@ public class TableConfigUtilsTest {
     TableConfigUtils.validate(tableConfig, schema);
 
     // valid transform configs
-    schema = new 
Schema.SchemaBuilder().setSchemaName(TABLE_NAME).addSingleValueDimension("myCol",
 DataType.STRING)
+    schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addSingleValueDimension("myCol", DataType.STRING)
+        .addDateTime(TIME_COLUMN, DataType.LONG, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
         .build();
     indexingConfig.setNoDictionaryColumns(List.of("myCol"));
     ingestionConfig.setAggregationConfigs(null);
@@ -390,35 +392,36 @@ public class TableConfigUtilsTest {
 
     Schema transformSchema = schema;
     ingestionConfig.setTransformConfigs(List.of(new TransformConfig("myCol", 
"now()")));
-    IllegalStateException nonDeterministicError =
-        expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validate(tableConfig, transformSchema));
-    assertTrue(nonDeterministicError.getMessage().contains("Function 'now' has 
VOLATILE volatility"));
+    TableConfigUtils.validate(tableConfig, transformSchema);
 
-    IngestionConfig existingIngestionConfig = new IngestionConfig();
-    existingIngestionConfig.setTransformConfigs(List.of(new 
TransformConfig("myCol", "now()")));
-    TableConfig existingTableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME)
-        .setIngestionConfig(existingIngestionConfig)
+    TableConfig realtimeTableConfig = new 
TableConfigBuilder(TableType.REALTIME).setTableName(TABLE_NAME)
+        .setTimeColumnName(TIME_COLUMN)
+        .setStreamConfigs(getStreamConfigs())
+        .setIngestionConfig(ingestionConfig)
         .build();
-    TableConfigUtils.validate(tableConfig, schema, null, existingTableConfig);
+    IllegalStateException nonDeterministicError = 
expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(realtimeTableConfig, transformSchema));
+    assertTrue(nonDeterministicError.getMessage().contains("Function 'now' has 
VOLATILE volatility"));
+    TableConfigUtils.validate(realtimeTableConfig, transformSchema, null, 
realtimeTableConfig);
 
     ingestionConfig.setTransformConfigs(List.of(new TransformConfig("myCol", 
"plus(now(), 1)")));
     nonDeterministicError = expectThrows(IllegalStateException.class,
-        () -> TableConfigUtils.validate(tableConfig, transformSchema, null, 
existingTableConfig));
+        () -> TableConfigUtils.validate(realtimeTableConfig, transformSchema));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'now' has 
VOLATILE volatility"));
 
     ingestionConfig.setTransformConfigs(List.of(new TransformConfig("myCol", 
"rand()")));
-    nonDeterministicError =
-        expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validate(tableConfig, transformSchema));
+    nonDeterministicError = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(realtimeTableConfig, transformSchema));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'rand' 
has VOLATILE volatility"));
 
     ingestionConfig.setTransformConfigs(List.of(new TransformConfig("myCol", 
"reqId('unused')")));
-    nonDeterministicError =
-        expectThrows(IllegalStateException.class, () -> 
TableConfigUtils.validate(tableConfig, transformSchema));
+    nonDeterministicError = expectThrows(IllegalStateException.class,
+        () -> TableConfigUtils.validate(realtimeTableConfig, transformSchema));
     assertTrue(nonDeterministicError.getMessage().contains("Function 'reqid' 
has STABLE volatility"),
         nonDeterministicError.getMessage());
 
     ingestionConfig.setTransformConfigs(List.of(new TransformConfig("myCol", 
"rand(123)")));
-    TableConfigUtils.validate(tableConfig, schema);
+    TableConfigUtils.validate(realtimeTableConfig, transformSchema);
 
     // Legacy schema-level transforms are also part of the ingestion pipeline. 
A new table must reject them, while
     // validation of an existing table stays permissive so unrelated config 
updates are not stranded.


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

Reply via email to