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

Jackie-Jiang 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 ca608fa3906 Fix NPE in GapfillProcessor when TIMESERIESON column is 
not in SELECT list (#18820)
ca608fa3906 is described below

commit ca608fa390634e64b76d614c3df7babd30d6aa5f
Author: Akanksha kedia <[email protected]>
AuthorDate: Thu Jun 25 00:30:28 2026 +0530

    Fix NPE in GapfillProcessor when TIMESERIESON column is not in SELECT list 
(#18820)
---
 .../pinot/core/query/reduce/GapfillProcessor.java  |  8 ++-
 .../org/apache/pinot/core/util/GapfillUtils.java   | 75 ++++++++++++++++++++++
 .../apache/pinot/queries/GapfillQueriesTest.java   | 18 ++++++
 3 files changed, 100 insertions(+), 1 deletion(-)

diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GapfillProcessor.java
 
b/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GapfillProcessor.java
index 16bf396a981..d07444f4981 100644
--- 
a/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GapfillProcessor.java
+++ 
b/pinot-core/src/main/java/org/apache/pinot/core/query/reduce/GapfillProcessor.java
@@ -41,6 +41,7 @@ import 
org.apache.pinot.core.query.aggregation.function.CountAggregationFunction
 import org.apache.pinot.core.query.aggregation.groupby.GroupByResultHolder;
 import org.apache.pinot.core.query.request.context.QueryContext;
 import org.apache.pinot.core.util.GapfillUtils;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 
 
 /**
@@ -89,7 +90,12 @@ public class GapfillProcessor extends BaseGapfillProcessor {
 
     // The first one argument of timeSeries is time column. The left ones are 
defining entity.
     for (ExpressionContext entityColum : _timeSeries) {
-      int index = indexes.get(entityColum.getIdentifier());
+      String colName = entityColum.getIdentifier();
+      Integer index = indexes.get(colName);
+      if (index == null) {
+        throw new BadQueryRequestException(
+            "TIMESERIESON column '" + colName + "' is not present in the 
SELECT list");
+      }
       _isGroupBySelections[index] = true;
     }
 
diff --git 
a/pinot-core/src/main/java/org/apache/pinot/core/util/GapfillUtils.java 
b/pinot-core/src/main/java/org/apache/pinot/core/util/GapfillUtils.java
index c75bdf15617..2dc0e81f30f 100644
--- a/pinot-core/src/main/java/org/apache/pinot/core/util/GapfillUtils.java
+++ b/pinot-core/src/main/java/org/apache/pinot/core/util/GapfillUtils.java
@@ -22,8 +22,10 @@ import com.google.common.base.Preconditions;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.pinot.common.request.Expression;
 import org.apache.pinot.common.request.ExpressionType;
 import org.apache.pinot.common.request.PinotQuery;
@@ -31,6 +33,7 @@ import 
org.apache.pinot.common.request.context.ExpressionContext;
 import org.apache.pinot.common.request.context.FunctionContext;
 import org.apache.pinot.common.utils.DataSchema;
 import org.apache.pinot.core.query.request.context.QueryContext;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 
 
 /**
@@ -282,6 +285,7 @@ public class GapfillUtils {
 
     // Carry over the query options from the original query
     Map<String, String> queryOptions = pinotQuery.getQueryOptions();
+    PinotQuery outerGapfillQuery = pinotQuery;
 
     while (pinotQuery.getDataSource().getSubquery() != null) {
       pinotQuery = pinotQuery.getDataSource().getSubquery();
@@ -322,10 +326,81 @@ public class GapfillUtils {
       }
     }
 
+    validateTimeSeriesOnColumns(outerGapfillQuery, strippedPinotQuery);
     strippedPinotQuery.setQueryOptions(queryOptions);
     return strippedPinotQuery;
   }
 
+  private static void validateTimeSeriesOnColumns(PinotQuery gapfillQuery, 
PinotQuery serverQuery) {
+    // Find the gapfill expression in the outer query
+    Expression gapfillExpr = null;
+    for (Expression select : gapfillQuery.getSelectList()) {
+      if (select.getType() != ExpressionType.FUNCTION) {
+        continue;
+      }
+      if (GAP_FILL.equals(select.getFunctionCall().getOperator())) {
+        gapfillExpr = select;
+        break;
+      }
+      if (AS.equals(select.getFunctionCall().getOperator())
+          && select.getFunctionCall().getOperands().get(0).getType() == 
ExpressionType.FUNCTION
+          && 
GAP_FILL.equals(select.getFunctionCall().getOperands().get(0).getFunctionCall().getOperator()))
 {
+        gapfillExpr = select.getFunctionCall().getOperands().get(0);
+        break;
+      }
+    }
+    if (gapfillExpr == null) {
+      return;
+    }
+
+    // Find the TIMESERIESON expression in the gapfill arguments
+    Expression timeSeriesOnExpr = null;
+    List<Expression> gapfillArgs = gapfillExpr.getFunctionCall().getOperands();
+    for (int i = STARTING_INDEX_OF_OPTIONAL_ARGS_FOR_PRE_AGGREGATE_GAP_FILL; i 
< gapfillArgs.size(); i++) {
+      Expression arg = gapfillArgs.get(i);
+      if (arg.getType() == ExpressionType.FUNCTION
+          && TIME_SERIES_ON.equals(arg.getFunctionCall().getOperator())) {
+        timeSeriesOnExpr = arg;
+        break;
+      }
+    }
+    if (timeSeriesOnExpr == null) {
+      return;
+    }
+
+    // Collect column names available in the server-side SELECT (identifiers 
and aliases)
+    Set<String> selectColumnNames = new HashSet<>();
+    for (Expression select : serverQuery.getSelectList()) {
+      if (select.getType() == ExpressionType.IDENTIFIER && 
select.getIdentifier() != null) {
+        selectColumnNames.add(select.getIdentifier().getName());
+      } else if (select.getType() == ExpressionType.FUNCTION
+          && AS.equals(select.getFunctionCall().getOperator())) {
+        List<Expression> asArgs = select.getFunctionCall().getOperands();
+        // Alias name is the second operand of AS
+        if (asArgs.size() >= 2 && asArgs.get(1).getType() == 
ExpressionType.IDENTIFIER
+            && asArgs.get(1).getIdentifier() != null) {
+          selectColumnNames.add(asArgs.get(1).getIdentifier().getName());
+        }
+        // Also include the inner expression when it is a bare identifier
+        if (!asArgs.isEmpty() && asArgs.get(0).getType() == 
ExpressionType.IDENTIFIER
+            && asArgs.get(0).getIdentifier() != null) {
+          selectColumnNames.add(asArgs.get(0).getIdentifier().getName());
+        }
+      }
+    }
+
+    // Validate each TIMESERIESON column is present in the server-side SELECT
+    for (Expression entityColumn : 
timeSeriesOnExpr.getFunctionCall().getOperands()) {
+      if (entityColumn.getType() == ExpressionType.IDENTIFIER && 
entityColumn.getIdentifier() != null) {
+        String colName = entityColumn.getIdentifier().getName();
+        if (!selectColumnNames.contains(colName)) {
+          throw new BadQueryRequestException(
+              "TIMESERIESON column '" + colName + "' is not present in the 
SELECT list");
+        }
+      }
+    }
+  }
+
   private static boolean hasGapfill(PinotQuery pinotQuery) {
     for (Expression select : pinotQuery.getSelectList()) {
       if (select.getType() != ExpressionType.FUNCTION) {
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/queries/GapfillQueriesTest.java 
b/pinot-core/src/test/java/org/apache/pinot/queries/GapfillQueriesTest.java
index ecf5c8ca822..228c33cb483 100644
--- a/pinot-core/src/test/java/org/apache/pinot/queries/GapfillQueriesTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/queries/GapfillQueriesTest.java
@@ -39,6 +39,7 @@ import org.apache.pinot.spi.data.DateTimeGranularitySpec;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.exception.BadQueryRequestException;
 import org.apache.pinot.spi.utils.ReadMode;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.testng.Assert;
@@ -4277,6 +4278,23 @@ public class GapfillQueriesTest extends BaseQueriesTest {
     }
   }
 
+  @Test
+  public void testGapfillThrowsOnTimeseriesOnColumnNotInSelect() {
+    // lotId is in TIMESERIESON but intentionally omitted from the SELECT list.
+    // Previously this caused a silent NPE (auto-unboxing a null Integer from 
the indexes map);
+    // now it must throw a BadQueryRequestException with a clear message.
+    String query = "SELECT GapFill(DATETIMECONVERT(eventTime, 
'1:MILLISECONDS:EPOCH', "
+        + "    '1:MILLISECONDS:SIMPLE_DATE_FORMAT:yyyy-MM-dd HH:mm:ss.SSS', 
'1:HOURS'), "
+        + "    '1:MILLISECONDS:SIMPLE_DATE_FORMAT:yyyy-MM-dd HH:mm:ss.SSS', "
+        + "    '2021-11-07 4:00:00.000', '2021-11-07 12:00:00.000', '1:HOURS', 
"
+        + "    FILL(isOccupied, 'FILL_DEFAULT_VALUE'), "
+        + "    TIMESERIESON(lotId)) AS time_col, isOccupied "
+        + "FROM parkingData "
+        + "WHERE eventTime >= 1636257600000 AND eventTime <= 1636286400000 "
+        + "LIMIT 200";
+    Assert.assertThrows(BadQueryRequestException.class, () -> 
getBrokerResponse(query));
+  }
+
   @AfterClass
   public void tearDown()
       throws IOException {


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

Reply via email to