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 8228534299a Reuse compiled constant string casts during planning 
(#19514)
8228534299a is described below

commit 8228534299a796107b6f9f4e1e39ebc94f4f866e
Author: Xiang Fu <[email protected]>
AuthorDate: Tue Sep 15 03:00:22 2026 -0700

    Reuse compiled constant string casts during planning (#19514)
---
 .../pinot/perf/BenchmarkConstantCastPlanning.java  | 145 ++++++
 .../pinot/perf/BenchmarkScalarCastPlanning.java    | 151 ++++++
 .../apache/pinot/calcite/rex/PinotRexExecutor.java | 130 +++++
 .../org/apache/pinot/query/QueryEnvironment.java   |  12 +
 .../pinot/calcite/rex/PinotRexExecutorTest.java    | 532 +++++++++++++++++++++
 .../pinot/query/ConstantCastPlanningTest.java      | 186 +++++++
 6 files changed, 1156 insertions(+)

diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkConstantCastPlanning.java
 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkConstantCastPlanning.java
new file mode 100644
index 00000000000..df2703f6eaa
--- /dev/null
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkConstantCastPlanning.java
@@ -0,0 +1,145 @@
+/**
+ * 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.perf;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.config.provider.StaticTableCache;
+import org.apache.pinot.query.QueryEnvironment;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+
+/// Measures SQL parsing, validation and logical optimization of timestamp 
range filters, with and without a window
+/// selecting the latest version of each event. CAST, EPOCH_STRING_CAST and 
TIMESTAMP_LITERAL use equivalent constants;
+/// EPOCH_MILLIS uses a LONG time column as a control without timestamp casts. 
No cluster or query execution is needed.
+/// Each benchmark thread owns its environment and cycles through prebuilt 
queries with different account IDs, keeping
+/// query-string construction outside the measured operation.
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(2)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class BenchmarkConstantCastPlanning {
+  private static final String TABLE_NAME = "events";
+  private static final int NUM_QUERIES = 1024;
+
+  @Param({"FILTER", "WINDOW"})
+  private String _queryShape;
+
+  @Param({"CAST", "EPOCH_STRING_CAST", "TIMESTAMP_LITERAL", "EPOCH_MILLIS"})
+  private String _literalForm;
+
+  private QueryEnvironment _queryEnvironment;
+  private String[] _queries;
+  private int _nextQuery;
+
+  @Setup
+  public void setUp() {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addSingleValueDimension("account_id", DataType.LONG)
+        .addSingleValueDimension("version", DataType.LONG)
+        .addSingleValueDimension("sequence_id", DataType.LONG)
+        .addMetric("reading", DataType.DOUBLE)
+        .addDateTime("event_time", DataType.TIMESTAMP, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
+        .addDateTime("event_time_ms", DataType.LONG, "1:MILLISECONDS:EPOCH", 
"1:MILLISECONDS")
+        .build();
+    schema.setEnableColumnBasedNullHandling(true);
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+    StaticTableCache tableCache = new StaticTableCache(List.of(tableConfig), 
List.of(schema), List.of(), false);
+    _queryEnvironment = new QueryEnvironment(QueryEnvironment.configBuilder()
+        .requestId(0L)
+        .database(CommonConstants.DEFAULT_DATABASE)
+        .tableCache(tableCache)
+        .isNullHandlingEnabled(true)
+        .defaultUsePhysicalOptimizer(false)
+        .build());
+
+    String timeColumn = "event_time";
+    String lowerBound;
+    String upperBound;
+    switch (_literalForm) {
+      case "CAST":
+        lowerBound = "CAST('2024-01-01 00:00:00' AS TIMESTAMP)";
+        upperBound = "CAST('2024-02-01 00:00:00' AS TIMESTAMP)";
+        break;
+      case "EPOCH_STRING_CAST":
+        lowerBound = "CAST('1704067200000' AS TIMESTAMP)";
+        upperBound = "CAST('1706745600000' AS TIMESTAMP)";
+        break;
+      case "TIMESTAMP_LITERAL":
+        lowerBound = "TIMESTAMP '2024-01-01 00:00:00'";
+        upperBound = "TIMESTAMP '2024-02-01 00:00:00'";
+        break;
+      case "EPOCH_MILLIS":
+        timeColumn = "event_time_ms";
+        lowerBound = "1704067200000";
+        upperBound = "1706745600000";
+        break;
+      default:
+        throw new IllegalArgumentException("Unknown literal form: " + 
_literalForm);
+    }
+
+    _queries = new String[NUM_QUERIES];
+    for (int i = 0; i < NUM_QUERIES; i++) {
+      String filter = " FROM " + TABLE_NAME + " WHERE account_id = " + 
(1000000L + i)
+          + " AND " + timeColumn + " >= " + lowerBound + " AND " + timeColumn 
+ " < " + upperBound;
+      switch (_queryShape) {
+        case "FILTER":
+          _queries[i] = "SELECT " + timeColumn + ", reading" + filter + " 
ORDER BY " + timeColumn + " LIMIT 1000";
+          break;
+        case "WINDOW":
+          _queries[i] = "SELECT " + timeColumn + ", reading FROM (SELECT " + 
timeColumn
+              + ", reading, ROW_NUMBER() OVER (PARTITION BY account_id, " + 
timeColumn
+              + " ORDER BY version DESC, sequence_id DESC) AS row_num" + filter
+              + ") WHERE row_num = 1 ORDER BY " + timeColumn + " LIMIT 1000";
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown query shape: " + 
_queryShape);
+      }
+    }
+    _nextQuery = 0;
+  }
+
+  @Benchmark
+  public Set<String> compile() {
+    String query = _queries[_nextQuery];
+    _nextQuery = (_nextQuery + 1) & (NUM_QUERIES - 1);
+    try (QueryEnvironment.CompiledQuery compiledQuery = 
_queryEnvironment.compile(query)) {
+      return compiledQuery.getTableNames();
+    }
+  }
+}
diff --git 
a/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkScalarCastPlanning.java
 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkScalarCastPlanning.java
new file mode 100644
index 00000000000..a3c7890d21a
--- /dev/null
+++ 
b/pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkScalarCastPlanning.java
@@ -0,0 +1,151 @@
+/**
+ * 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.perf;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.apache.pinot.common.config.provider.StaticTableCache;
+import org.apache.pinot.query.QueryEnvironment;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.CommonConstants;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+
+/// Measures SQL parsing, validation and logical optimization of scalar 
filters using constant string casts or
+/// equivalent numeric/boolean literals. Predicates compare each constant with 
a matching column type. DECIMAL uses
+/// the schema's default precision and scale. No cluster or query execution is 
needed.
+/// Each thread owns its environment and cycles through 1,024 prebuilt 
queries. Numeric cast values vary with each
+/// query; boolean values alternate, with a varying account filter keeping the 
queries distinct. Query construction
+/// stays outside the measured operation, and warmup populates reusable 
conversion templates.
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@Fork(2)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@State(Scope.Thread)
+public class BenchmarkScalarCastPlanning {
+  private static final String TABLE_NAME = "scalar_events";
+  private static final int NUM_QUERIES = 1024;
+
+  @Param({"INTEGER", "BIGINT", "DOUBLE", "DECIMAL", "BOOLEAN"})
+  private String _dataType;
+
+  @Param({"CAST", "LITERAL"})
+  private String _literalForm;
+
+  private QueryEnvironment _queryEnvironment;
+  private String[] _queries;
+  private int _nextQuery;
+
+  @Setup
+  public void setUp() {
+    Schema schema = new Schema.SchemaBuilder().setSchemaName(TABLE_NAME)
+        .addSingleValueDimension("account_id", DataType.LONG)
+        .addSingleValueDimension("int_value", DataType.INT)
+        .addSingleValueDimension("long_value", DataType.LONG)
+        .addSingleValueDimension("double_value", DataType.DOUBLE)
+        .addSingleValueDimension("decimal_value", DataType.BIG_DECIMAL)
+        .addSingleValueDimension("boolean_value", DataType.BOOLEAN)
+        .build();
+    schema.setEnableColumnBasedNullHandling(true);
+    TableConfig tableConfig = new 
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+    StaticTableCache tableCache = new StaticTableCache(List.of(tableConfig), 
List.of(schema), List.of(), false);
+    _queryEnvironment = new QueryEnvironment(QueryEnvironment.configBuilder()
+        .requestId(0L)
+        .database(CommonConstants.DEFAULT_DATABASE)
+        .tableCache(tableCache)
+        .isNullHandlingEnabled(true)
+        .defaultUsePhysicalOptimizer(false)
+        .build());
+
+    _queries = new String[NUM_QUERIES];
+    for (int i = 0; i < NUM_QUERIES; i++) {
+      String column;
+      String value;
+      String literal;
+      switch (_dataType) {
+        case "INTEGER":
+          column = "int_value";
+          value = Integer.toString(1000000 + i);
+          literal = value;
+          break;
+        case "BIGINT":
+          column = "long_value";
+          value = Long.toString(5000000000L + i);
+          literal = value;
+          break;
+        case "DOUBLE":
+          column = "double_value";
+          value = (1000000 + i) + ".25";
+          literal = value + "E0";
+          break;
+        case "DECIMAL":
+          column = "decimal_value";
+          value = (1000000 + i) + ".25";
+          literal = value;
+          break;
+        case "BOOLEAN":
+          column = "boolean_value";
+          value = (i & 1) == 0 ? "true" : "false";
+          literal = value;
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown data type: " + 
_dataType);
+      }
+      String constant;
+      switch (_literalForm) {
+        case "CAST":
+          constant = "CAST('" + value + "' AS " + _dataType + ")";
+          break;
+        case "LITERAL":
+          constant = literal;
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown literal form: " + 
_literalForm);
+      }
+      _queries[i] = "SELECT account_id, " + column + " FROM " + TABLE_NAME + " 
WHERE " + column + " = " + constant
+          + " AND account_id >= " + (1000000L + i) + " ORDER BY account_id 
LIMIT 1000";
+    }
+    _nextQuery = 0;
+  }
+
+  @Benchmark
+  public Set<String> compile() {
+    String query = _queries[_nextQuery];
+    _nextQuery = (_nextQuery + 1) & (NUM_QUERIES - 1);
+    try (QueryEnvironment.CompiledQuery compiledQuery = 
_queryEnvironment.compile(query)) {
+      return compiledQuery.getTableNames();
+    }
+  }
+}
diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rex/PinotRexExecutor.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rex/PinotRexExecutor.java
new file mode 100644
index 00000000000..5d4757f0ec3
--- /dev/null
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/calcite/rex/PinotRexExecutor.java
@@ -0,0 +1,130 @@
+/**
+ * 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.calcite.rex;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import org.apache.calcite.DataContext;
+import org.apache.calcite.DataContexts;
+import org.apache.calcite.linq4j.function.Function1;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexExecutor;
+import org.apache.calcite.rex.RexExecutorImpl;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+
+
+/// Reduces string literal casts using bounded, reusable Calcite executables. 
Each source/target type pair is compiled
+/// with an input reference instead of a literal, so different values share 
Calcite's conversion code. Other expressions
+/// use the fallback executor. Cached functions are immutable; input values 
and results stay local to each call, making
+/// this executor thread-safe without sharing RexExecutable's mutable data 
context.
+public final class PinotRexExecutor implements RexExecutor {
+  public static final PinotRexExecutor INSTANCE = new 
PinotRexExecutor(RexUtil.EXECUTOR);
+
+  private final RexExecutor _fallback;
+  private final Cache<CastSignature, Function1<DataContext, Object[]>> _casts;
+
+  @VisibleForTesting
+  PinotRexExecutor(RexExecutor fallback) {
+    this(fallback, 256);
+  }
+
+  @VisibleForTesting
+  PinotRexExecutor(RexExecutor fallback, int cacheSize) {
+    _fallback = fallback;
+    _casts = CacheBuilder.newBuilder().maximumSize(cacheSize).build();
+  }
+
+  @Override
+  public void reduce(RexBuilder rexBuilder, List<RexNode> constExps, 
List<RexNode> reducedValues) {
+    // Calcite treats reduction as all-or-nothing. Delegate the entire batch 
if any expression is unsupported, so a
+    // failure in the fallback cannot leave the batch partially reduced.
+    for (RexNode expression : constExps) {
+      if (!isSupportedStringCast(expression)) {
+        _fallback.reduce(rexBuilder, constExps, reducedValues);
+        return;
+      }
+    }
+    List<RexNode> literals = new ArrayList<>(constExps.size());
+    try {
+      for (RexNode expression : constExps) {
+        RexLiteral operand = (RexLiteral) ((RexCall) 
expression).getOperands().get(0);
+        Object value = null;
+        if (!operand.isNull()) {
+          Function1<DataContext, Object[]> cast = getCast(rexBuilder, 
operand.getType(), expression.getType());
+          DataContext context = DataContexts.of(Map.of("inputRecord", new 
Object[]{RexLiteral.stringValue(operand)}));
+          value = cast.apply(context)[0];
+        }
+        literals.add(rexBuilder.makeLiteral(value, expression.getType(), 
true));
+      }
+    } catch (RuntimeException | ExecutionException e) {
+      // Like Calcite, retain the entire batch if conversion, compilation or 
literal construction fails. In particular,
+      // invalid epoch strings must remain available for Pinot's later 
timestamp conversion.
+      reducedValues.addAll(constExps);
+      return;
+    }
+    reducedValues.addAll(literals);
+  }
+
+  @VisibleForTesting
+  Function1<DataContext, Object[]> getCast(RexBuilder builder, RelDataType 
source, RelDataType target)
+      throws ExecutionException {
+    CastSignature signature = new CastSignature(source, target, 
builder.getTypeFactory().getTypeSystem());
+    return _casts.get(signature, () -> {
+      RelDataType rowType = builder.getTypeFactory().builder().add("value", 
source).build();
+      RexNode cast = builder.makeAbstractCast(target, 
builder.makeInputRef(source, 0), false);
+      return RexExecutorImpl.getExecutable(builder, List.of(cast), 
rowType).getFunction();
+    });
+  }
+
+  private static boolean isSupportedStringCast(RexNode expression) {
+    if (!(expression instanceof RexCall)) {
+      return false;
+    }
+    RexCall call = (RexCall) expression;
+    if (call.getOperator() != SqlStdOperatorTable.CAST || 
call.getOperands().size() != 1) {
+      return false;
+    }
+    // Keep timezone-dependent and structured conversions in the fallback's 
own data context.
+    RelDataType target = call.getType();
+    if (!SqlTypeUtil.isAtomic(target) || 
SqlTypeName.TZ_TYPES.contains(target.getSqlTypeName())
+        || target.getSqlTypeName() == SqlTypeName.VARIANT) {
+      return false;
+    }
+    RexNode operand = call.getOperands().get(0);
+    return operand instanceof RexLiteral && 
SqlTypeUtil.isCharacter(operand.getType());
+  }
+
+  /// Full types retain precision, scale, nullability, charset and collation; 
the type system supplies rounding rules.
+  /// Literal values never enter the key or the generated code.
+  private record CastSignature(RelDataType source, RelDataType target, 
RelDataTypeSystem typeSystem) {
+  }
+}
diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
index bfa24cac481..3f30e35547a 100644
--- 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
@@ -44,6 +44,7 @@ import org.apache.calcite.prepare.CalciteCatalogReader;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelRoot;
 import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexExecutor;
 import org.apache.calcite.runtime.CalciteContextException;
 import org.apache.calcite.sql.SqlExplain;
 import org.apache.calcite.sql.SqlExplainFormat;
@@ -62,6 +63,7 @@ import 
org.apache.pinot.calcite.rel.rules.PinotJoinToDynamicBroadcastRule;
 import org.apache.pinot.calcite.rel.rules.PinotRelDistributionTraitRule;
 import org.apache.pinot.calcite.rel.rules.PinotRuleUtils;
 import org.apache.pinot.calcite.rel.rules.PinotSortExchangeCopyRule;
+import org.apache.pinot.calcite.rex.PinotRexExecutor;
 import org.apache.pinot.calcite.sql.fun.PinotOperatorTable;
 import org.apache.pinot.calcite.sql2rel.PinotConvertletTable;
 import org.apache.pinot.calcite.sql2rel.PinotRelDecorrelator;
@@ -446,6 +448,13 @@ public class QueryEnvironment {
   ///
   /// It is important to notice that the returned tree is not yet 
[optimized][#optimize(RelRoot, PlannerContext)].
   private RelRoot toRelation(SqlNode sqlNode, PlannerContext plannerContext) {
+    RelOptPlanner planner = plannerContext.getRelOptPlanner();
+    RexExecutor originalExecutor = planner.getExecutor();
+    if (originalExecutor == null) {
+      // SqlToRelConverter transforms its RelBuilder, discarding executors 
provided only through the builder context.
+      // Install on the per-query planner so conversion and field trimming can 
reuse compiled cast templates.
+      planner.setExecutor(PinotRexExecutor.INSTANCE);
+    }
     try {
       RexBuilder rexBuilder = new RexBuilder(_typeFactory);
       RelOptCluster cluster = 
RelOptCluster.create(plannerContext.getRelOptPlanner(), rexBuilder);
@@ -481,6 +490,9 @@ public class QueryEnvironment {
     } catch (Throwable e) {
       throw QueryErrorCode.QUERY_PLANNING.asException(
           "Error converting query to relational expression: " + 
e.getMessage(), e);
+    } finally {
+      // Keep the executor policy of the subsequent optimization phase 
unchanged.
+      planner.setExecutor(originalExecutor);
     }
   }
 
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/calcite/rex/PinotRexExecutorTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/calcite/rex/PinotRexExecutorTest.java
new file mode 100644
index 00000000000..89351688c6b
--- /dev/null
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/calcite/rex/PinotRexExecutorTest.java
@@ -0,0 +1,532 @@
+/**
+ * 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.calcite.rex;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import org.apache.calcite.DataContext;
+import org.apache.calcite.DataContexts;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.linq4j.function.Function1;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexExecutor;
+import org.apache.calcite.rex.RexExecutorImpl;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.runtime.Hook;
+import org.apache.calcite.sql.SqlCollation;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.NlsString;
+import org.apache.pinot.calcite.rel.rules.PinotRuleUtils;
+import org.apache.pinot.query.type.TypeFactory;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotSame;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+
+/// Compares scalar constant reduction with Calcite and verifies reuse of 
immutable conversion templates.
+public class PinotRexExecutorTest {
+  private static final RexExecutor CALCITE_EXECUTOR = new 
RexExecutorImpl(DataContexts.EMPTY);
+  private static final RexExecutor FAILING_FALLBACK = (builder, expressions, 
results) -> {
+    throw new AssertionError("Supported casts must not use the fallback: " + 
expressions);
+  };
+
+  @DataProvider
+  public Object[][] scalarLiterals() {
+    List<Object[]> cases = new ArrayList<>();
+    addScalarCases(cases, SqlTypeName.TINYINT, -1, -1, "127", "-128", "128", 
"-129", " 42 ", "1.5", "invalid", null);
+    addScalarCases(cases, SqlTypeName.SMALLINT, -1, -1, "32767", "-32768", 
"32768", "-32769", " 42 ", "invalid", null);
+    addScalarCases(cases, SqlTypeName.INTEGER, -1, -1, "2147483647", 
"-2147483648", "2147483648", "-2147483649",
+        "\t42\n", "1.5", "invalid", null);
+    addScalarCases(cases, SqlTypeName.UTINYINT, -1, -1, "1", "255", "256", 
"-1", null);
+    addScalarCases(cases, SqlTypeName.USMALLINT, -1, -1, "1", "65535", 
"65536", "-1", null);
+    addScalarCases(cases, SqlTypeName.UINTEGER, -1, -1, "1", "4294967295", 
"4294967296", "-1", null);
+    addScalarCases(cases, SqlTypeName.UBIGINT, -1, -1, "1", 
"18446744073709551615", "18446744073709551616", "-1", null);
+    for (SqlTypeName target : List.of(SqlTypeName.FLOAT, SqlTypeName.REAL, 
SqlTypeName.DOUBLE)) {
+      addScalarCases(cases, target, -1, -1, "1.25", "-1.5", "1e100", " 1.25 ", 
"NaN", "invalid", null);
+    }
+    addScalarCases(cases, SqlTypeName.DECIMAL, 5, 2, "123.455", "-123.455", 
"999.995", " 1.25 ", "invalid", null);
+    addScalarCases(cases, SqlTypeName.DECIMAL, 8, 3, "123.4555", "-123.4555", 
"99999.9995", " 1.25 ", "invalid", null);
+    addScalarCases(cases, SqlTypeName.BOOLEAN, -1, -1, "true", "FALSE", " true 
", "1", "invalid", null);
+    addScalarCases(cases, SqlTypeName.DATE, -1, -1, "2000-02-29", 
"1969-12-31", " 2000-02-29 ", "2026-02-30",
+        "invalid", null);
+    for (int precision : List.of(0, 3)) {
+      addScalarCases(cases, SqlTypeName.TIME, precision, -1, "12:34:56.9876", 
"23:59:59.9999", " 12:34:56 ",
+          "25:00:00", "invalid", null);
+    }
+    addScalarCases(cases, SqlTypeName.UUID, -1, -1, 
"123e4567-e89b-12d3-a456-426614174000",
+        " 123e4567-e89b-12d3-a456-426614174000 ", "123", "invalid", null);
+    for (SqlTypeName target : List.of(SqlTypeName.CHAR, SqlTypeName.VARCHAR, 
SqlTypeName.BINARY,
+        SqlTypeName.VARBINARY)) {
+      addScalarCases(cases, target, 3, -1, "ab", "abcdef", " a ", "", null);
+    }
+    return cases.toArray(new Object[0][]);
+  }
+
+  @Test(dataProvider = "scalarLiterals")
+  public void testScalarCastsMatchCalciteWithoutFallback(SqlTypeName 
sourceType, SqlTypeName targetType,
+      int precision, int scale, String value, boolean validControl) {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataTypeFactory factory = builder.getTypeFactory();
+    RelDataType target;
+    if (precision < 0) {
+      target = factory.createSqlType(targetType);
+    } else if (scale < 0) {
+      target = factory.createSqlType(targetType, precision);
+    } else {
+      target = factory.createSqlType(targetType, precision, scale);
+    }
+    target = factory.createTypeWithNullability(target, true);
+    RelDataType source = factory.createSqlType(sourceType, 64);
+    RexNode operand = value == null ? builder.makeNullLiteral(source) : 
builder.stringLiteral(value, source);
+    RexNode expression = builder.makeAbstractCast(target, operand, false);
+    List<RexNode> input = List.of(expression);
+    List<RexNode> expected = reduce(CALCITE_EXECUTOR, builder, input);
+    if (SqlTypeName.UNSIGNED_TYPES.contains(targetType) && value != null) {
+      // Calcite can evaluate unsigned casts but cannot materialize their 
non-null RexLiterals yet.
+      assertEquals(expected, input, "Unsigned casts must remain unchanged when 
literal construction is unsupported");
+    } else if (validControl) {
+      assertFalse(expected.equals(input), "The valid control must reduce for " 
+ target);
+    }
+    List<RexNode> actual = reduce(new PinotRexExecutor(FAILING_FALLBACK), 
builder, input);
+    assertEquals(actual, expected);
+    assertEquals(actual.get(0).getType(), expected.get(0).getType());
+    if (value == null) {
+      assertTrue(RexLiteral.isNullLiteral(actual.get(0)));
+    } else if (expected.equals(input)) {
+      assertSame(actual.get(0), expression, "A failed cast must retain the 
original expression");
+    }
+  }
+
+  private static void addScalarCases(List<Object[]> cases, SqlTypeName target, 
int precision, int scale,
+      String... values) {
+    for (SqlTypeName source : List.of(SqlTypeName.CHAR, SqlTypeName.VARCHAR)) {
+      for (int i = 0; i < values.length; i++) {
+        cases.add(new Object[]{source, target, precision, scale, values[i], i 
== 0});
+      }
+    }
+  }
+
+  @DataProvider
+  public Object[][] bigintLiterals() {
+    List<Object[]> cases = new ArrayList<>();
+    for (SqlTypeName sourceType : List.of(SqlTypeName.CHAR, 
SqlTypeName.VARCHAR)) {
+      for (String value : new String[]{"0", "-0", "-1", "+1", "000123", " 123 
", "\t123\n", "9007199254740993",
+          "9223372036854775807", "-9223372036854775808", 
"9223372036854775808", "-9223372036854775809",
+          "1.0", "1e3", "", "invalid", null}) {
+        cases.add(new Object[]{sourceType, value});
+      }
+    }
+    return cases.toArray(new Object[0][]);
+  }
+
+  @Test(dataProvider = "bigintLiterals")
+  public void testBigintCastsMatchCalciteWithoutFallback(SqlTypeName 
sourceType, String value) {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataType source = builder.getTypeFactory().createSqlType(sourceType, 
30);
+    RelDataType target = builder.getTypeFactory().createTypeWithNullability(
+        builder.getTypeFactory().createSqlType(SqlTypeName.BIGINT), true);
+    RexNode operand = value == null ? builder.makeNullLiteral(source) : 
builder.stringLiteral(value, sourceType);
+    RexNode expression = builder.makeAbstractCast(target, operand, false);
+    List<RexNode> input = List.of(expression);
+    CountingFallback fallback = new CountingFallback(CALCITE_EXECUTOR);
+    List<RexNode> expected = reduce(CALCITE_EXECUTOR, builder, input);
+    List<RexNode> actual = reduce(new PinotRexExecutor(fallback), builder, 
input);
+    assertEquals(actual, expected);
+    assertEquals(actual.get(0).getType(), expected.get(0).getType());
+    assertEquals(fallback._calls.get(), 0);
+    if (expected.equals(input)) {
+      assertSame(actual.get(0), expression);
+    }
+  }
+
+  @Test
+  public void testBigintMixedBatchesMatchCalcite() {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataType bigint = 
builder.getTypeFactory().createSqlType(SqlTypeName.BIGINT);
+    RexNode valid = builder.makeAbstractCast(bigint, 
builder.makeLiteral("123"), false);
+    RexNode invalid = builder.makeAbstractCast(bigint, 
builder.makeLiteral("invalid"), false);
+    RexNode timestamp = timestampCast(builder, SqlTypeName.CHAR, "2026-04-01 
00:00:00", 3);
+    RexNode unsupported = builder.makeCall(SqlStdOperatorTable.PLUS, 
builder.makeExactLiteral(BigDecimal.ONE),
+        builder.makeExactLiteral(BigDecimal.TEN));
+    for (List<RexNode> input : List.of(List.of(valid, invalid), 
List.of(invalid, valid), List.of(valid, timestamp),
+        List.of(timestamp, valid), List.of(valid, unsupported))) {
+      CountingFallback fallback = new CountingFallback(CALCITE_EXECUTOR);
+      List<RexNode> actual = reduce(new PinotRexExecutor(fallback), builder, 
input);
+      assertEquals(actual, reduce(CALCITE_EXECUTOR, builder, input));
+      assertEquals(fallback._calls.get(), input.contains(unsupported) ? 1 : 0);
+    }
+  }
+
+  @DataProvider
+  public Object[][] timestampLiterals() {
+    List<Object[]> cases = new ArrayList<>();
+    for (SqlTypeName sourceType : List.of(SqlTypeName.CHAR, 
SqlTypeName.VARCHAR)) {
+      for (String value : List.of("2026-09-01 00:00:00", "1970-01-01 
00:00:00", "1969-12-31 23:59:59.999",
+          "2000-02-29 12:34:56.123", "2026-09-01 12:34:56.1", "2026-09-01 
12:34:56.123456789",
+          "2026-09-01 23:59:59.9999", "1969-12-31 23:59:59.9999")) {
+        cases.add(new Object[]{sourceType, value, 3});
+      }
+      for (int precision : List.of(0, 1, 2)) {
+        cases.add(new Object[]{sourceType, "1969-12-31 23:59:59.987654", 
precision});
+        cases.add(new Object[]{sourceType, "2026-09-01 12:34:56.987654", 
precision});
+      }
+      cases.add(new Object[]{sourceType, "2026-09-01 23:59:59.9999", 0});
+      cases.add(new Object[]{sourceType, "1969-12-31 23:59:59.9999", 0});
+    }
+    return cases.toArray(new Object[0][]);
+  }
+
+  @Test(dataProvider = "timestampLiterals")
+  public void testSupportedCastsDoNotUseFallback(SqlTypeName sourceType, 
String value, int precision) {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RexNode expression = timestampCast(builder, sourceType, value, precision);
+    assertEquals(expression.getKind(), SqlKind.CAST);
+    List<RexNode> expected = reduce(CALCITE_EXECUTOR, builder, 
List.of(expression));
+    assertFalse(expected.get(0).equals(expression), "The Calcite control must 
reduce the test expression");
+
+    List<RexNode> actual = reduce(new PinotRexExecutor(FAILING_FALLBACK), 
builder, List.of(expression));
+    assertEquals(actual, expected);
+    assertEquals(actual.get(0).getType(), expected.get(0).getType());
+  }
+
+  @Test
+  public void testNullCastsPreserveTypeAndNullabilityWithoutFallback() {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    for (SqlTypeName sourceType : List.of(SqlTypeName.CHAR, 
SqlTypeName.VARCHAR)) {
+      RelDataType type = builder.getTypeFactory().createSqlType(sourceType, 
20);
+      RelDataType target = builder.getTypeFactory().createTypeWithNullability(
+          builder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP, 3), 
true);
+      RexNode expression = builder.makeAbstractCast(target, 
builder.makeNullLiteral(type), false);
+      List<RexNode> expected = reduce(CALCITE_EXECUTOR, builder, 
List.of(expression));
+      List<RexNode> actual = reduce(new PinotRexExecutor(FAILING_FALLBACK), 
builder, List.of(expression));
+      assertEquals(actual, expected);
+      assertTrue(RexLiteral.isNullLiteral(actual.get(0)));
+      assertEquals(actual.get(0).getType(), expected.get(0).getType());
+    }
+  }
+
+  @DataProvider
+  public Object[][] timestampEdgeInputs() {
+    return new Object[][]{
+        {"2026-09-01"}, {" 2026-09-01 12:34:56.123 "}, {"2026-09-01 
12:34:56.123   "},
+        {"\t2026-09-01 12:34:56.123\n"}, {""}, {"not-a-timestamp"}, 
{"2026-02-30 00:00:00"},
+        {"2026-09-01 25:00:00"}, {"2026-09-01T12:34:56Z"}, {"2026-09-01 
12:34:56+02:00"},
+        {"1788266096123"}
+    };
+  }
+
+  @Test(dataProvider = "timestampEdgeInputs")
+  public void testWhitespaceAndInvalidInputsMatchCalcite(String value) {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RexNode expression = timestampCast(builder, SqlTypeName.CHAR, value, 3);
+    List<RexNode> input = List.of(expression);
+    List<RexNode> expected = reduce(CALCITE_EXECUTOR, builder, input);
+    CountingFallback fallback = new CountingFallback(CALCITE_EXECUTOR);
+
+    List<RexNode> actual = reduce(new PinotRexExecutor(fallback), builder, 
input);
+    assertEquals(actual, expected);
+    assertEquals(fallback._calls.get(), 0, "Eligible casts must bypass the 
fallback even when conversion fails");
+    if (expected.equals(input)) {
+      assertSame(actual.get(0), expression, "Failed casts must retain the 
original expression");
+    }
+  }
+
+  @Test
+  public void testUnsupportedExpressionsDelegateTheWholeBatchInOrder() {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RexNode fast = timestampCast(builder, SqlTypeName.CHAR, "2026-09-01 
12:34:56.123", 3);
+    RelDataType timestamp = 
builder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP, 3);
+    RexLiteral string = builder.stringLiteral("2026-09-01 12:34:56.123", 
SqlTypeName.CHAR);
+    List<RexNode> unsupported = List.of(
+        builder.makeCall(SqlStdOperatorTable.PLUS, 
builder.makeExactLiteral(BigDecimal.ONE),
+            builder.makeExactLiteral(BigDecimal.TEN)),
+        builder.makeAbstractCast(timestamp, string, true),
+        builder.makeAbstractCast(timestamp, string, false, 
builder.makeLiteral("YYYY-MM-DD HH24:MI:SS.FF")),
+        builder.makeAbstractCast(timestamp, builder.makeAbstractCast(
+            builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR, 30), 
string, false), false),
+        builder.makeCall(timestamp, new SqlSpecialOperator("CAST", 
SqlKind.CAST), List.of(string)));
+
+    for (RexNode expression : unsupported) {
+      List<RexNode> input = List.of(fast, expression, fast);
+      CountingFallback fallback = new CountingFallback(CALCITE_EXECUTOR);
+      List<RexNode> actual = reduce(new PinotRexExecutor(fallback), builder, 
input);
+      assertEquals(actual, reduce(CALCITE_EXECUTOR, builder, input));
+      assertEquals(fallback._calls.get(), 1);
+      assertSame(fallback._lastInput, input, "Fallback must receive the 
original full batch");
+      assertSame(input.get(0), fast, "The immutable input must not be 
modified");
+    }
+  }
+
+  @Test
+  public void testFailedCastDoesNotPartiallyReduceBatch() {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RexNode fast = timestampCast(builder, SqlTypeName.CHAR, "2026-09-01 
12:34:56.123", 3);
+    RexNode invalid = timestampCast(builder, SqlTypeName.VARCHAR, 
"not-a-timestamp", 3);
+    for (List<RexNode> input : List.of(List.of(fast, invalid), 
List.of(invalid, fast))) {
+      CountingFallback fallback = new CountingFallback(CALCITE_EXECUTOR);
+      List<RexNode> actual = reduce(new PinotRexExecutor(fallback), builder, 
input);
+      assertEquals(actual, reduce(CALCITE_EXECUTOR, builder, input));
+      assertEquals(actual, input, "Calcite keeps every expression unchanged 
when one expression fails");
+      assertEquals(fallback._calls.get(), 0, "Failed eligible casts must not 
retry through the fallback");
+      for (int i = 0; i < input.size(); i++) {
+        assertSame(actual.get(i), input.get(i), "The entire failed batch must 
retain its original expressions");
+      }
+    }
+  }
+
+  @Test
+  public void testTimezoneDependentTargetsUseTheirFallbackContext() {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RexNode expression = builder.makeAbstractCast(
+        
builder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE,
 3),
+        builder.stringLiteral("2026-09-01 12:34:56.123", SqlTypeName.CHAR), 
false);
+    List<RexNode> input = List.of(expression);
+    for (String zone : List.of("UTC", "America/Los_Angeles", "Asia/Kolkata")) {
+      RexExecutor baseline = new RexExecutorImpl(DataContexts.of(
+          Map.of(DataContext.Variable.TIME_ZONE.camelName, 
TimeZone.getTimeZone(zone))));
+      CountingFallback fallback = new CountingFallback(baseline);
+      assertEquals(reduce(new PinotRexExecutor(fallback), builder, input), 
reduce(baseline, builder, input), zone);
+      assertEquals(fallback._calls.get(), 1);
+      assertSame(fallback._lastInput, input);
+    }
+  }
+
+  @Test
+  public void testCastTemplateIsReusedAcrossLiteralValues() throws Exception {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataType source = 
builder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR, 20);
+    RelDataType target = 
builder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+    PinotRexExecutor executor = new PinotRexExecutor(FAILING_FALLBACK);
+    Function1<DataContext, Object[]> template = executor.getCast(builder, 
source, target);
+    for (String value : List.of("1", "2345", "-67")) {
+      List<RexNode> input = List.of(builder.makeAbstractCast(target, 
builder.stringLiteral(value, source), false));
+      assertEquals(reduce(executor, builder, input), reduce(CALCITE_EXECUTOR, 
builder, input));
+      assertSame(executor.getCast(new LiteralRexBuilder(), source, target), 
template,
+          "Literal values and equivalent builders must reuse the same compiled 
function");
+    }
+  }
+
+  @Test
+  public void testCastCacheDistinguishesFullTypesAndTypeSystem() throws 
Exception {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataTypeFactory factory = builder.getTypeFactory();
+    RelDataType source = factory.createSqlType(SqlTypeName.VARCHAR, 20);
+    RelDataType target = factory.createSqlType(SqlTypeName.DECIMAL, 8, 2);
+    PinotRexExecutor executor = new PinotRexExecutor(FAILING_FALLBACK);
+    Function1<DataContext, Object[]> template = executor.getCast(builder, 
source, target);
+    SqlCollation latinCollation = new 
SqlCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
+        StandardCharsets.ISO_8859_1, "primary");
+    for (RelDataType otherSource : 
List.of(factory.createSqlType(SqlTypeName.CHAR, 20),
+        factory.createSqlType(SqlTypeName.VARCHAR, 21), 
factory.createTypeWithNullability(source, true),
+        factory.createTypeWithCharsetAndCollation(source, 
StandardCharsets.ISO_8859_1, latinCollation))) {
+      assertNotSame(executor.getCast(builder, otherSource, target), template, 
otherSource.getFullTypeString());
+    }
+    for (RelDataType otherTarget : 
List.of(factory.createSqlType(SqlTypeName.DECIMAL, 9, 2),
+        factory.createSqlType(SqlTypeName.DECIMAL, 8, 3), 
factory.createTypeWithNullability(target, true))) {
+      assertNotSame(executor.getCast(builder, source, otherTarget), template, 
otherTarget.getFullTypeString());
+    }
+    RelDataType textTarget = factory.createSqlType(SqlTypeName.VARCHAR, 8);
+    RelDataType latinTarget = 
factory.createTypeWithCharsetAndCollation(textTarget, 
StandardCharsets.ISO_8859_1,
+        latinCollation);
+    assertNotSame(executor.getCast(builder, source, textTarget), 
executor.getCast(builder, source, latinTarget));
+    LiteralRexBuilder otherBuilder = new LiteralRexBuilder(new 
JavaTypeFactoryImpl(new RelDataTypeSystemImpl() {
+      @Override
+      public RoundingMode roundingMode() {
+        return RoundingMode.FLOOR;
+      }
+    }));
+    assertNotSame(executor.getCast(otherBuilder, source, target), template,
+        "Different type systems must not share compiled rounding semantics");
+    List<RexNode> input = List.of(builder.makeAbstractCast(target, 
builder.stringLiteral("-1.239", source), false));
+    for (RexBuilder rexBuilder : List.of(builder, otherBuilder)) {
+      assertEquals(reduce(executor, rexBuilder, input), 
reduce(CALCITE_EXECUTOR, rexBuilder, input));
+    }
+    assertSame(executor.getCast(builder, source, target), template);
+  }
+
+  @Test
+  public void testCastCacheEvictsOldTemplates() throws Exception {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    RelDataTypeFactory factory = builder.getTypeFactory();
+    RelDataType source = factory.createSqlType(SqlTypeName.VARCHAR, 20);
+    RelDataType integer = factory.createSqlType(SqlTypeName.INTEGER);
+    RelDataType bigint = factory.createSqlType(SqlTypeName.BIGINT);
+    RelDataType decimal = factory.createSqlType(SqlTypeName.DECIMAL, 8, 2);
+    PinotRexExecutor executor = new PinotRexExecutor(FAILING_FALLBACK, 2);
+    Function1<DataContext, Object[]> first = executor.getCast(builder, source, 
integer);
+    Function1<DataContext, Object[]> second = executor.getCast(builder, 
source, bigint);
+    executor.getCast(builder, source, decimal);
+    assertSame(executor.getCast(builder, source, bigint), second, "A recently 
compiled template should remain cached");
+    assertNotSame(executor.getCast(builder, source, integer), first, "The 
oldest template must be evicted at capacity");
+    List<RexNode> input = List.of(builder.makeAbstractCast(integer, 
builder.stringLiteral("123", source), false));
+    assertEquals(reduce(executor, builder, input), reduce(CALCITE_EXECUTOR, 
builder, input));
+  }
+
+  @Test
+  public void testSharedExecutorHasNoCrossRequestState() throws Exception {
+    PinotRexExecutor executor = new PinotRexExecutor(FAILING_FALLBACK);
+    ExecutorService threads = Executors.newFixedThreadPool(4);
+    CountDownLatch ready = new CountDownLatch(4);
+    try {
+      List<Callable<Void>> tasks = new ArrayList<>();
+      for (int task = 0; task < 16; task++) {
+        LiteralRexBuilder localBuilder = new LiteralRexBuilder();
+        RelDataType source = 
localBuilder.getTypeFactory().createSqlType(SqlTypeName.VARCHAR, 20);
+        RelDataType target = 
localBuilder.getTypeFactory().createSqlType(SqlTypeName.INTEGER);
+        List<RexNode> input = List.of(localBuilder.makeAbstractCast(target,
+            localBuilder.stringLiteral(Integer.toString(100 + task), source), 
false));
+        List<RexNode> expected = reduce(CALCITE_EXECUTOR, localBuilder, input);
+        tasks.add(() -> {
+          ready.countDown();
+          assertTrue(ready.await(30, TimeUnit.SECONDS), "All workers must 
reach the fresh cache together");
+          for (int i = 0; i < 10; i++) {
+            assertEquals(reduce(executor, localBuilder, input), expected);
+          }
+          return null;
+        });
+      }
+      for (Future<Void> result : threads.invokeAll(tasks)) {
+        result.get();
+      }
+    } finally {
+      threads.shutdownNow();
+    }
+  }
+
+  @Test
+  @SuppressWarnings("try") // The resources scope the thread-local hooks; 
their values are intentionally unused.
+  public void testRelBuilderTransformPreservesOptimizedExecutor() {
+    AtomicInteger generatedReductions = new AtomicInteger();
+    RelNode expected;
+    try (Hook.Closeable ignored = Hook.EXPRESSION_REDUCER.addThread(
+        (Consumer<Object>) event -> generatedReductions.incrementAndGet())) {
+      expected = projectTimestampCast(CALCITE_EXECUTOR);
+    }
+    assertTrue(generatedReductions.get() > 0, "The control must exercise 
Calcite's generated reducer");
+
+    generatedReductions.set(0);
+    RelNode actual;
+    try (Hook.Closeable ignored = Hook.EXPRESSION_REDUCER.addThread(
+        (Consumer<Object>) event -> generatedReductions.incrementAndGet())) {
+      actual = projectTimestampCast(PinotRexExecutor.INSTANCE);
+    }
+    assertEquals(RelOptUtil.toString(actual), RelOptUtil.toString(expected));
+    assertEquals(generatedReductions.get(), 0, "RelBuilder.transform must 
preserve the planner's optimized executor");
+  }
+
+  @Test
+  public void testRelBuilderTransformPreservesExplicitExecutor() {
+    CountingFallback explicitExecutor = new CountingFallback(CALCITE_EXECUTOR);
+    RelNode actual = projectTimestampCast(explicitExecutor);
+    assertTrue(explicitExecutor._calls.get() > 0, "An explicit planner 
executor must retain precedence");
+    assertEquals(RelOptUtil.toString(actual), 
RelOptUtil.toString(projectTimestampCast(CALCITE_EXECUTOR)));
+  }
+
+  private static RelNode projectTimestampCast(RexExecutor executor) {
+    LiteralRexBuilder builder = new LiteralRexBuilder();
+    VolcanoPlanner planner = new VolcanoPlanner();
+    planner.setExecutor(executor);
+    RelBuilder relBuilder = 
PinotRuleUtils.PINOT_REL_FACTORY.create(RelOptCluster.create(planner, builder), 
null)
+        .transform(config -> config.withPruneInputOfAggregate(true));
+    RelNode result = relBuilder.values(new String[]{"unused"}, 0)
+        .project(timestampCast(builder, SqlTypeName.CHAR, "2026-09-01 
12:34:56.123", 3)).build();
+    assertSame(planner.getExecutor(), executor, "RelBuilder.transform must not 
replace the planner executor");
+    return result;
+  }
+
+  private static RexNode timestampCast(LiteralRexBuilder builder, SqlTypeName 
sourceType, String value,
+      int precision) {
+    // makeCast may fold a literal before it reaches the executor, which would 
make a dispatch test vacuous.
+    return 
builder.makeAbstractCast(builder.getTypeFactory().createSqlType(SqlTypeName.TIMESTAMP,
 precision),
+        builder.stringLiteral(value, sourceType), false);
+  }
+
+  private static List<RexNode> reduce(RexExecutor executor, RexBuilder 
builder, List<RexNode> expressions) {
+    List<RexNode> reduced = new ArrayList<>();
+    executor.reduce(builder, expressions, reduced);
+    return reduced;
+  }
+
+  private static class CountingFallback implements RexExecutor {
+    private final RexExecutor _delegate;
+    private final AtomicInteger _calls = new AtomicInteger();
+    private List<RexNode> _lastInput;
+
+    CountingFallback(RexExecutor delegate) {
+      _delegate = delegate;
+    }
+
+    @Override
+    public void reduce(RexBuilder builder, List<RexNode> expressions, 
List<RexNode> reduced) {
+      _calls.incrementAndGet();
+      _lastInput = expressions;
+      _delegate.reduce(builder, expressions, reduced);
+    }
+  }
+
+  private static class LiteralRexBuilder extends RexBuilder {
+    LiteralRexBuilder() {
+      this(new TypeFactory());
+    }
+
+    LiteralRexBuilder(RelDataTypeFactory typeFactory) {
+      super(typeFactory);
+    }
+
+    RexLiteral stringLiteral(String value, SqlTypeName sourceType) {
+      // Preserve VARCHAR as the source SQL type; the public literal helper 
normally lowers it to CHAR or a CAST.
+      return stringLiteral(value, getTypeFactory().createSqlType(sourceType, 
Math.max(1, value.length())));
+    }
+
+    RexLiteral stringLiteral(String value, RelDataType type) {
+      return makeLiteral(new NlsString(value, null, null), type, 
SqlTypeName.CHAR);
+    }
+  }
+}
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/ConstantCastPlanningTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/ConstantCastPlanningTest.java
new file mode 100644
index 00000000000..bc87ed4292b
--- /dev/null
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/ConstantCastPlanningTest.java
@@ -0,0 +1,186 @@
+/**
+ * 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.query;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.runtime.Hook;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.pinot.sql.parsers.CalciteSqlParser;
+import org.apache.pinot.sql.parsers.SqlNodeAndOptions;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+
+/// Verifies that SQL-to-rel conversion reduces constant string casts without 
changing the resulting plan,
+/// and restores the planner's executor afterward. The expression-reduction 
hook is thread-local and
+/// closed after each compilation.
+public class ConstantCastPlanningTest extends QueryEnvironmentTestBase {
+  @DataProvider
+  public Object[][] timestampQueries() {
+    return new Object[][]{
+        {false, "account_1", false},
+        {false, "account_2", false},
+        {true, "account_1", false},
+        {true, "account_2", false},
+        {false, "account_1", true},
+        {false, "account_2", true},
+        {true, "account_1", true},
+        {true, "account_2", true}
+    };
+  }
+
+  @Test(dataProvider = "timestampQueries")
+  // The hook resource installs and removes the callback; it is not otherwise 
referenced.
+  @SuppressWarnings("try")
+  public void testConstantTimestampCastPlanning(boolean window, String 
accountId, boolean epochString) {
+    String lowerBound = epochString ? "1704067200000" : "2024-01-01 00:00:00";
+    String upperBound = epochString ? "1706745600000" : "2024-02-01 00:00:00";
+    String castQuery = query(window, accountId, "CAST('" + lowerBound + "' AS 
TIMESTAMP)",
+        "CAST('" + upperBound + "' AS TIMESTAMP)");
+    String literalQuery = query(window, accountId, "TIMESTAMP '2024-01-01 
00:00:00'",
+        "TIMESTAMP '2024-02-01 00:00:00'");
+    SqlNodeAndOptions parsedQuery = 
CalciteSqlParser.compileToSqlNodeAndOptions(castQuery);
+    AtomicInteger parsedCasts = new AtomicInteger();
+    parsedQuery.getSqlNode().accept(new SqlBasicVisitor<Void>() {
+      @Override
+      public Void visit(SqlCall call) {
+        if (call.getKind() == SqlKind.CAST) {
+          parsedCasts.incrementAndGet();
+        }
+        return super.visit(call);
+      }
+    });
+    assertEquals(parsedCasts.get(), 2, "The query must reach compilation with 
both string-to-timestamp casts");
+
+    AtomicInteger reductions = new AtomicInteger();
+    try (QueryEnvironment.CompiledQuery literalPlan = 
_queryEnvironment.compile(literalQuery);
+        Hook.Closeable ignored = Hook.EXPRESSION_REDUCER.addThread(value -> {
+          reductions.incrementAndGet();
+        });
+        QueryEnvironment.CompiledQuery castPlan = 
_queryEnvironment.compile(castQuery, parsedQuery)) {
+      assertEquals(RelOptUtil.toString(castPlan.getRelNode()), 
RelOptUtil.toString(literalPlan.getRelNode()));
+      assertEquals(castPlan.getRelRoot().validatedRowType, 
literalPlan.getRelRoot().validatedRowType);
+      assertEquals(castPlan.getTableNames(), literalPlan.getTableNames());
+      assertNull(castPlan.getPlannerContext().getRelOptPlanner().getExecutor(),
+          "SQL-to-rel conversion must restore the planner's executor before 
optimization");
+      assertEquals(reductions.get(), 0, "Timestamp casts must use cached 
templates instead of the fallback reducer");
+    }
+  }
+
+  @DataProvider
+  public Object[][] bigintQueries() {
+    List<Object[]> cases = new ArrayList<>();
+    for (String value : List.of("0", "123", "9007199254740993", 
"9223372036854775807", "-9223372036854775808")) {
+      for (boolean explicitCast : List.of(false, true)) {
+        for (boolean window : List.of(false, true)) {
+          cases.add(new Object[]{value, explicitCast, window});
+        }
+      }
+    }
+    return cases.toArray(new Object[0][]);
+  }
+
+  @Test(dataProvider = "bigintQueries")
+  @SuppressWarnings("try") // The resource scopes the thread-local 
fallback-reducer hook.
+  public void testConstantBigintCastPlanning(String value, boolean 
explicitCast, boolean window) {
+    String quoted = explicitCast ? "CAST('" + value + "' AS BIGINT)" : "'" + 
value + "'";
+    String numeric = "CAST(" + value + " AS BIGINT)";
+    AtomicInteger reductions = new AtomicInteger();
+    try (QueryEnvironment.CompiledQuery literalPlan = 
_queryEnvironment.compile(bigintQuery(numeric, window));
+        Hook.Closeable ignored = Hook.EXPRESSION_REDUCER.addThread(v -> {
+          reductions.incrementAndGet();
+        });
+        QueryEnvironment.CompiledQuery castPlan = 
_queryEnvironment.compile(bigintQuery(quoted, window))) {
+      assertEquals(RelOptUtil.toString(castPlan.getRelNode()), 
RelOptUtil.toString(literalPlan.getRelNode()));
+      assertEquals(castPlan.getRelRoot().validatedRowType, 
literalPlan.getRelRoot().validatedRowType);
+      assertEquals(castPlan.getTableNames(), literalPlan.getTableNames());
+      
assertNull(castPlan.getPlannerContext().getRelOptPlanner().getExecutor());
+      assertEquals(reductions.get(), 0, "BIGINT casts must use cached 
templates instead of the fallback reducer");
+    }
+  }
+
+  @DataProvider
+  public Object[][] scalarCastQueries() {
+    return new Object[][]{
+        {"INTEGER", "2147483647", "CAST(2147483647 AS INTEGER)", 
SqlTypeName.INTEGER},
+        {"SMALLINT", "32767", "CAST(32767 AS SMALLINT)", SqlTypeName.SMALLINT},
+        {"TINYINT", "127", "CAST(127 AS TINYINT)", SqlTypeName.TINYINT},
+        {"REAL", "1.25", "CAST(1.25 AS REAL)", SqlTypeName.REAL},
+        {"FLOAT", "1.25", "CAST(1.25 AS FLOAT)", SqlTypeName.FLOAT},
+        {"DOUBLE", "1.25", "CAST(1.25 AS DOUBLE)", SqlTypeName.DOUBLE},
+        {"DECIMAL(6, 2)", "1234.50", "CAST(1234.50 AS DECIMAL(6, 2))", 
SqlTypeName.DECIMAL},
+        {"DECIMAL(20, 4)", "9007199254740993.1250", 
"CAST(9007199254740993.1250 AS DECIMAL(20, 4))",
+            SqlTypeName.DECIMAL},
+        {"BOOLEAN", "true", "TRUE", SqlTypeName.BOOLEAN},
+        {"BOOLEAN", "false", "FALSE", SqlTypeName.BOOLEAN},
+        {"DATE", "2024-02-29", "DATE '2024-02-29'", SqlTypeName.DATE},
+        {"TIME", "12:34:56", "TIME '12:34:56'", SqlTypeName.TIME},
+        {"CHAR(3)", "abcdef", "'abc'", SqlTypeName.CHAR}
+    };
+  }
+
+  @Test(dataProvider = "scalarCastQueries")
+  public void testConstantScalarCastPlanning(String targetType, String value, 
String literal,
+      SqlTypeName expectedType) {
+    String castQuery = "SELECT CAST('" + value + "' AS " + targetType + ") AS 
cast_value, col3 FROM a WHERE col3 > 0";
+    String literalQuery = "SELECT " + literal + " AS cast_value, col3 FROM a 
WHERE col3 > 0";
+    try (QueryEnvironment.CompiledQuery literalPlan = 
_queryEnvironment.compile(literalQuery);
+        QueryEnvironment.CompiledQuery castPlan = 
_queryEnvironment.compile(castQuery)) {
+      assertEquals(RelOptUtil.toString(castPlan.getRelNode()), 
RelOptUtil.toString(literalPlan.getRelNode()));
+      assertEquals(castPlan.getRelRoot().validatedRowType, 
literalPlan.getRelRoot().validatedRowType);
+      
assertEquals(castPlan.getRelRoot().validatedRowType.getFieldList().get(0).getType().getSqlTypeName(),
+          expectedType);
+      assertEquals(castPlan.getTableNames(), literalPlan.getTableNames());
+      
assertNull(castPlan.getPlannerContext().getRelOptPlanner().getExecutor());
+      // Exercise conversion to dispatchable stages as well as SQL-to-rel 
conversion and optimization.
+      assertNotNull(literalPlan.planQuery(0).getQueryPlan());
+      assertNotNull(castPlan.planQuery(0).getQueryPlan());
+    }
+  }
+
+  private static String bigintQuery(String value, boolean window) {
+    String filter = " FROM a WHERE col7 = " + value;
+    if (window) {
+      return "SELECT col7, col3 FROM (SELECT col7, col3, ROW_NUMBER() OVER "
+          + "(PARTITION BY col7 ORDER BY col3 DESC) AS row_num" + filter + ") 
WHERE row_num = 1";
+    }
+    return "SELECT col7, col3" + filter;
+  }
+
+  private static String query(boolean window, String accountId, String 
lowerBound, String upperBound) {
+    String filter = " FROM a WHERE col1 = '" + accountId + "' AND ts_timestamp 
>= " + lowerBound
+        + " AND ts_timestamp < " + upperBound;
+    if (window) {
+      return "SELECT ts_timestamp, col3 FROM (SELECT ts_timestamp, col3, 
ROW_NUMBER() OVER "
+          + "(PARTITION BY col1, ts_timestamp ORDER BY col3 DESC, col7 DESC) 
AS row_num" + filter
+          + ") WHERE row_num = 1 ORDER BY ts_timestamp LIMIT 1000";
+    }
+    return "SELECT ts_timestamp, col3" + filter + " ORDER BY ts_timestamp 
LIMIT 1000";
+  }
+}


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

Reply via email to