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]