xiangfu0 commented on code in PR #19011:
URL: https://github.com/apache/pinot/pull/19011#discussion_r3698672838


##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java:
##########
@@ -464,4 +480,107 @@ protected void onQueryFinish(long requestId) {
   protected boolean isQueryCancellationEnabled() {
     return _enableQueryCancellation;
   }
+
+  /// Appends a where clause to the query to filter out of retention data if 
SKIP_OUT_OF_RETENTION_VALUES is
+  /// set to True in query Options.
+  /// @param sqlNodeAndOptions
+  private void applySkipOutOfRetentionValuesIfNeeded(SqlNodeAndOptions 
sqlNodeAndOptions) {
+    Map<String, String> options = sqlNodeAndOptions.getOptions();
+    if 
(!Boolean.parseBoolean(options.get(QueryOptionKey.SKIP_OUT_OF_RETENTION_VALUES)))
 {
+      return;
+    }
+    SqlNode sqlNode = sqlNodeAndOptions.getSqlNode();
+    if (sqlNode == null) {
+      return;
+    }
+
+    // Resolve the inner SqlSelect, unwrapping SqlOrderBy if needed.
+    // SqlWith (CTEs) and other statement types are not supported.
+    SqlSelect sqlSelect;
+    if (sqlNode instanceof SqlSelect) {
+      sqlSelect = (SqlSelect) sqlNode;
+    } else if (sqlNode instanceof SqlOrderBy) {
+      SqlNode query = ((SqlOrderBy) sqlNode).query;
+      if (!(query instanceof SqlSelect)) {
+        return;
+      }
+      sqlSelect = (SqlSelect) query;
+    } else {
+      return;
+    }
+
+    TableNameExtractor tableNameExtractor = new TableNameExtractor();
+    tableNameExtractor.extractTableNames(sqlSelect);
+    Set<String> tableNames = tableNameExtractor.getTableNames();
+    if (tableNames.isEmpty()) {
+      return;
+    }
+
+    // Multi-table queries (JOINs) are not supported: it is ambiguous which 
table's retention
+    // applies, and injecting a filter with the wrong table's time column 
would produce
+    // incorrect results. Callers must issue separate single-table queries.
+    if (tableNames.size() > 1) {
+      LOGGER.debug("skipOutOfRetentionValues is not supported for multi-table 
queries; skipping filter injection");
+      return;
+    }
+
+    String rawTableName = 
TableNameBuilder.extractRawTableName(tableNames.iterator().next());
+
+    TableConfig tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.OFFLINE.tableNameWithType(rawTableName));

Review Comment:
   **[Critical] Resolve the queried physical table before reading retention.** 
`extractRawTableName()` drops an explicit `_REALTIME` suffix, then this lookup 
always prefers `<raw>_OFFLINE`. For a hybrid table with 30-day OFFLINE and 
7-day REALTIME retention, a query against `foo_REALTIME` injects the 30-day 
cutoff and silently includes expired rows. This also runs before database and 
case canonicalization, so qualified or differently-cased names can miss or 
select the wrong config. Preserve the explicit type, resolve the canonical 
fully qualified table, and derive retention per physical scan.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java:
##########
@@ -464,4 +480,107 @@ protected void onQueryFinish(long requestId) {
   protected boolean isQueryCancellationEnabled() {
     return _enableQueryCancellation;
   }
+
+  /// Appends a where clause to the query to filter out of retention data if 
SKIP_OUT_OF_RETENTION_VALUES is
+  /// set to True in query Options.
+  /// @param sqlNodeAndOptions
+  private void applySkipOutOfRetentionValuesIfNeeded(SqlNodeAndOptions 
sqlNodeAndOptions) {
+    Map<String, String> options = sqlNodeAndOptions.getOptions();
+    if 
(!Boolean.parseBoolean(options.get(QueryOptionKey.SKIP_OUT_OF_RETENTION_VALUES)))
 {
+      return;
+    }
+    SqlNode sqlNode = sqlNodeAndOptions.getSqlNode();
+    if (sqlNode == null) {
+      return;
+    }
+
+    // Resolve the inner SqlSelect, unwrapping SqlOrderBy if needed.
+    // SqlWith (CTEs) and other statement types are not supported.
+    SqlSelect sqlSelect;
+    if (sqlNode instanceof SqlSelect) {
+      sqlSelect = (SqlSelect) sqlNode;
+    } else if (sqlNode instanceof SqlOrderBy) {
+      SqlNode query = ((SqlOrderBy) sqlNode).query;
+      if (!(query instanceof SqlSelect)) {
+        return;
+      }
+      sqlSelect = (SqlSelect) query;
+    } else {
+      return;
+    }
+
+    TableNameExtractor tableNameExtractor = new TableNameExtractor();
+    tableNameExtractor.extractTableNames(sqlSelect);
+    Set<String> tableNames = tableNameExtractor.getTableNames();
+    if (tableNames.isEmpty()) {
+      return;
+    }
+
+    // Multi-table queries (JOINs) are not supported: it is ambiguous which 
table's retention
+    // applies, and injecting a filter with the wrong table's time column 
would produce
+    // incorrect results. Callers must issue separate single-table queries.
+    if (tableNames.size() > 1) {
+      LOGGER.debug("skipOutOfRetentionValues is not supported for multi-table 
queries; skipping filter injection");
+      return;
+    }
+
+    String rawTableName = 
TableNameBuilder.extractRawTableName(tableNames.iterator().next());
+
+    TableConfig tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.OFFLINE.tableNameWithType(rawTableName));
+    if (tableConfig == null) {
+      tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.REALTIME.tableNameWithType(rawTableName));
+    }
+    Schema schema = _tableCache.getSchema(rawTableName);
+    if (tableConfig == null || schema == null || 
tableConfig.getValidationConfig() == null) {
+      return;
+    }
+
+    //get timestamp column and retention details
+    SegmentsValidationAndRetentionConfig validationConfig = 
tableConfig.getValidationConfig();
+    String timeColumnName = validationConfig.getTimeColumnName();
+    if (StringUtils.isEmpty(timeColumnName) || 
!validationConfig.hasRetention()) {
+      return;
+    }
+    try {
+      long retentionTimeMs = validationConfig.getRetentionTimeMillis();
+      long cutoffMs = System.currentTimeMillis() - retentionTimeMs;
+      DateTimeFieldSpec timeFieldSpec = 
schema.getSpecForTimeColumn(timeColumnName);
+      if (timeFieldSpec == null) {
+        return;
+      }
+
+      DateTimeFormatSpec formatSpec = new 
DateTimeFormatSpec(timeFieldSpec.getFormat());
+      String formattedCutoffTime = formatSpec.fromMillisToFormat(cutoffMs);
+
+      //add where clause to the AST
+      SqlIdentifier timeColNode = new SqlIdentifier(timeColumnName, 
SqlParserPos.ZERO);
+      SqlLiteral cutoffNode;
+      if (timeFieldSpec.getDataType().isNumeric()) {
+        cutoffNode = SqlLiteral.createExactNumeric(formattedCutoffTime, 
SqlParserPos.ZERO);
+      } else {
+        cutoffNode = SqlLiteral.createCharString(formattedCutoffTime, 
SqlParserPos.ZERO);

Review Comment:
   **[Critical] Avoid direct string comparison for variable-width date 
formats.** An accepted STRING time format such as `yyyy-M-d` can produce cutoff 
`2025-10-1`; an expired stored value `2025-9-30` compares lexicographically 
greater and therefore passes this predicate. Schema validation checks component 
order but not fixed width. Compare converted epoch values or reject formats 
that are not guaranteed to be lexicographically sortable, and add STRING/SDF 
boundary tests.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java:
##########
@@ -464,4 +480,107 @@ protected void onQueryFinish(long requestId) {
   protected boolean isQueryCancellationEnabled() {
     return _enableQueryCancellation;
   }
+
+  /// Appends a where clause to the query to filter out of retention data if 
SKIP_OUT_OF_RETENTION_VALUES is
+  /// set to True in query Options.
+  /// @param sqlNodeAndOptions
+  private void applySkipOutOfRetentionValuesIfNeeded(SqlNodeAndOptions 
sqlNodeAndOptions) {
+    Map<String, String> options = sqlNodeAndOptions.getOptions();
+    if 
(!Boolean.parseBoolean(options.get(QueryOptionKey.SKIP_OUT_OF_RETENTION_VALUES)))
 {
+      return;
+    }
+    SqlNode sqlNode = sqlNodeAndOptions.getSqlNode();
+    if (sqlNode == null) {
+      return;
+    }
+
+    // Resolve the inner SqlSelect, unwrapping SqlOrderBy if needed.
+    // SqlWith (CTEs) and other statement types are not supported.
+    SqlSelect sqlSelect;

Review Comment:
   **[Major] Apply this rewrite at resolved table scans.** CTE and 
set-operation roots are silently skipped, while a derived-table query finds the 
inner physical table but attaches the predicate to this outer `SqlSelect`. For 
example, `SELECT * FROM (SELECT category FROM foo) t` receives an outer 
`eventTime` reference that does not exist. Equivalent single-table SQL can 
therefore execute unfiltered or fail based only on syntax. Use a planner-level 
scan rewrite, or explicitly reject unsupported shapes.



##########
pinot-integration-tests/src/test/resources/log4j2-test.xml:
##########
@@ -30,7 +30,7 @@
     <Console name="spammy" target="SYSTEM_OUT">
       <PatternLayout pattern="%d{HH:mm:ss.SSS} %p [%c{1}] [%t] %m%n"/>
       <Filters>
-        <BurstFilter level="ERROR" rate="1" maxBurst="2"/>
+        <BurstFilter level="DEBUG" rate="1" maxBurst="2"/>

Review Comment:
   **[Major] Revert this unrelated logging change.** The attached accounting 
logger emits WARN and above; with a DEBUG BurstFilter threshold, those 
WARN/ERROR events bypass rate limiting across the integration suite. Restore 
`ERROR`, or isolate any additional logging required by this test.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java:
##########
@@ -464,4 +480,107 @@ protected void onQueryFinish(long requestId) {
   protected boolean isQueryCancellationEnabled() {
     return _enableQueryCancellation;
   }
+
+  /// Appends a where clause to the query to filter out of retention data if 
SKIP_OUT_OF_RETENTION_VALUES is
+  /// set to True in query Options.
+  /// @param sqlNodeAndOptions
+  private void applySkipOutOfRetentionValuesIfNeeded(SqlNodeAndOptions 
sqlNodeAndOptions) {
+    Map<String, String> options = sqlNodeAndOptions.getOptions();
+    if 
(!Boolean.parseBoolean(options.get(QueryOptionKey.SKIP_OUT_OF_RETENTION_VALUES)))
 {
+      return;
+    }
+    SqlNode sqlNode = sqlNodeAndOptions.getSqlNode();
+    if (sqlNode == null) {
+      return;
+    }
+
+    // Resolve the inner SqlSelect, unwrapping SqlOrderBy if needed.
+    // SqlWith (CTEs) and other statement types are not supported.
+    SqlSelect sqlSelect;
+    if (sqlNode instanceof SqlSelect) {
+      sqlSelect = (SqlSelect) sqlNode;
+    } else if (sqlNode instanceof SqlOrderBy) {
+      SqlNode query = ((SqlOrderBy) sqlNode).query;
+      if (!(query instanceof SqlSelect)) {
+        return;
+      }
+      sqlSelect = (SqlSelect) query;
+    } else {
+      return;
+    }
+
+    TableNameExtractor tableNameExtractor = new TableNameExtractor();
+    tableNameExtractor.extractTableNames(sqlSelect);
+    Set<String> tableNames = tableNameExtractor.getTableNames();
+    if (tableNames.isEmpty()) {
+      return;
+    }
+
+    // Multi-table queries (JOINs) are not supported: it is ambiguous which 
table's retention
+    // applies, and injecting a filter with the wrong table's time column 
would produce
+    // incorrect results. Callers must issue separate single-table queries.
+    if (tableNames.size() > 1) {
+      LOGGER.debug("skipOutOfRetentionValues is not supported for multi-table 
queries; skipping filter injection");
+      return;
+    }
+
+    String rawTableName = 
TableNameBuilder.extractRawTableName(tableNames.iterator().next());
+
+    TableConfig tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.OFFLINE.tableNameWithType(rawTableName));
+    if (tableConfig == null) {
+      tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.REALTIME.tableNameWithType(rawTableName));
+    }
+    Schema schema = _tableCache.getSchema(rawTableName);
+    if (tableConfig == null || schema == null || 
tableConfig.getValidationConfig() == null) {
+      return;
+    }
+
+    //get timestamp column and retention details
+    SegmentsValidationAndRetentionConfig validationConfig = 
tableConfig.getValidationConfig();
+    String timeColumnName = validationConfig.getTimeColumnName();
+    if (StringUtils.isEmpty(timeColumnName) || 
!validationConfig.hasRetention()) {
+      return;
+    }
+    try {
+      long retentionTimeMs = validationConfig.getRetentionTimeMillis();
+      long cutoffMs = System.currentTimeMillis() - retentionTimeMs;
+      DateTimeFieldSpec timeFieldSpec = 
schema.getSpecForTimeColumn(timeColumnName);
+      if (timeFieldSpec == null) {
+        return;
+      }
+
+      DateTimeFormatSpec formatSpec = new 
DateTimeFormatSpec(timeFieldSpec.getFormat());
+      String formattedCutoffTime = formatSpec.fromMillisToFormat(cutoffMs);
+
+      //add where clause to the AST
+      SqlIdentifier timeColNode = new SqlIdentifier(timeColumnName, 
SqlParserPos.ZERO);
+      SqlLiteral cutoffNode;
+      if (timeFieldSpec.getDataType().isNumeric()) {
+        cutoffNode = SqlLiteral.createExactNumeric(formattedCutoffTime, 
SqlParserPos.ZERO);
+      } else {
+        cutoffNode = SqlLiteral.createCharString(formattedCutoffTime, 
SqlParserPos.ZERO);
+      }
+
+      SqlBasicCall rangeFilter = new SqlBasicCall(
+          SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, List.of(timeColNode, 
cutoffNode),
+          SqlParserPos.ZERO);
+
+      SqlNode existingWhere = sqlSelect.getWhere();
+      if (existingWhere != null) {
+        //add an AND if the where exists
+        SqlBasicCall andExpr = new SqlBasicCall(
+            SqlStdOperatorTable.AND,
+            List.of(existingWhere, rangeFilter),
+            SqlParserPos.ZERO
+        );
+        sqlSelect.setWhere(andExpr);
+      } else {
+        sqlSelect.setWhere(rangeFilter);
+      }
+      LOGGER.debug("Injected Calcite AST filter for skipOutOfRetentionValues 
on table: {}. Cutoff: {}", rawTableName,
+          formattedCutoffTime);
+    } catch (RuntimeException e) {

Review Comment:
   **[Major] Do not silently fail open when the option cannot be honored.** 
`hasRetention()` only checks that the two strings are nonempty; a malformed 
retention value passes that guard, throws during conversion, and reaches this 
catch, after which the original unfiltered query executes with no 
client-visible warning. Validate a positive numeric retention value during 
table admission and return a query-validation error when an enabled rewrite 
cannot be applied.



##########
pinot-broker/src/main/java/org/apache/pinot/broker/requesthandler/BaseBrokerRequestHandler.java:
##########
@@ -464,4 +480,107 @@ protected void onQueryFinish(long requestId) {
   protected boolean isQueryCancellationEnabled() {
     return _enableQueryCancellation;
   }
+
+  /// Appends a where clause to the query to filter out of retention data if 
SKIP_OUT_OF_RETENTION_VALUES is
+  /// set to True in query Options.
+  /// @param sqlNodeAndOptions
+  private void applySkipOutOfRetentionValuesIfNeeded(SqlNodeAndOptions 
sqlNodeAndOptions) {
+    Map<String, String> options = sqlNodeAndOptions.getOptions();
+    if 
(!Boolean.parseBoolean(options.get(QueryOptionKey.SKIP_OUT_OF_RETENTION_VALUES)))
 {
+      return;
+    }
+    SqlNode sqlNode = sqlNodeAndOptions.getSqlNode();
+    if (sqlNode == null) {
+      return;
+    }
+
+    // Resolve the inner SqlSelect, unwrapping SqlOrderBy if needed.
+    // SqlWith (CTEs) and other statement types are not supported.
+    SqlSelect sqlSelect;
+    if (sqlNode instanceof SqlSelect) {
+      sqlSelect = (SqlSelect) sqlNode;
+    } else if (sqlNode instanceof SqlOrderBy) {
+      SqlNode query = ((SqlOrderBy) sqlNode).query;
+      if (!(query instanceof SqlSelect)) {
+        return;
+      }
+      sqlSelect = (SqlSelect) query;
+    } else {
+      return;
+    }
+
+    TableNameExtractor tableNameExtractor = new TableNameExtractor();
+    tableNameExtractor.extractTableNames(sqlSelect);
+    Set<String> tableNames = tableNameExtractor.getTableNames();
+    if (tableNames.isEmpty()) {
+      return;
+    }
+
+    // Multi-table queries (JOINs) are not supported: it is ambiguous which 
table's retention
+    // applies, and injecting a filter with the wrong table's time column 
would produce
+    // incorrect results. Callers must issue separate single-table queries.
+    if (tableNames.size() > 1) {
+      LOGGER.debug("skipOutOfRetentionValues is not supported for multi-table 
queries; skipping filter injection");
+      return;
+    }
+
+    String rawTableName = 
TableNameBuilder.extractRawTableName(tableNames.iterator().next());
+
+    TableConfig tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.OFFLINE.tableNameWithType(rawTableName));
+    if (tableConfig == null) {
+      tableConfig = 
_tableCache.getTableConfig(TableNameBuilder.REALTIME.tableNameWithType(rawTableName));
+    }
+    Schema schema = _tableCache.getSchema(rawTableName);
+    if (tableConfig == null || schema == null || 
tableConfig.getValidationConfig() == null) {
+      return;
+    }
+
+    //get timestamp column and retention details
+    SegmentsValidationAndRetentionConfig validationConfig = 
tableConfig.getValidationConfig();
+    String timeColumnName = validationConfig.getTimeColumnName();
+    if (StringUtils.isEmpty(timeColumnName) || 
!validationConfig.hasRetention()) {
+      return;
+    }
+    try {
+      long retentionTimeMs = validationConfig.getRetentionTimeMillis();
+      long cutoffMs = System.currentTimeMillis() - retentionTimeMs;
+      DateTimeFieldSpec timeFieldSpec = 
schema.getSpecForTimeColumn(timeColumnName);
+      if (timeFieldSpec == null) {
+        return;
+      }
+
+      DateTimeFormatSpec formatSpec = new 
DateTimeFormatSpec(timeFieldSpec.getFormat());

Review Comment:
   **[Major] Reuse the cached format specification.** 
`DateTimeFieldSpec.getFormatSpec()` already caches and safely publishes the 
parsed `DateTimeFormatSpec`. Constructing another one here reparses the schema 
format and allocates formatter state on every opted-in query. Use 
`timeFieldSpec.getFormatSpec()`.



##########
pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/SkipOutOfRetentionValuesIntegrationTest.java:
##########
@@ -0,0 +1,171 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.integration.tests;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.common.utils.TarCompressionUtils;
+import 
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.apache.pinot.util.TestUtils;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+
+
+ /// Integration test for the `skipOutOfRetentionValues` query option.
+ /// Verifies that when the query option is provided, the broker dynamically 
injects a time
+ /// filter based on the table's retention configuration and pushes it down to 
the servers.
+public class SkipOutOfRetentionValuesIntegrationTest extends 
BaseClusterIntegrationTest {

Review Comment:
   **[Major] Use the shared custom integration-test cluster.** This test only 
defines ordinary schema, data, and SQL assertions, yet starts a dedicated 
ZK/controller/broker/server cluster. Move it under `integration/tests/custom`, 
extend `CustomDataQueryClusterIntegrationTest`, add `@Test(suiteName = 
"CustomClusterIntegrationTest")`, and include it in the shared custom-cluster 
suite.



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