diff --git a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RexExpressionUtils.java b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RexExpressionUtils.java index 278e34ac0741..8bcf2bf6b634 100644 --- a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RexExpressionUtils.java +++ b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/RexExpressionUtils.java @@ -34,6 +34,8 @@ import org.apache.calcite.avatica.util.ByteString; import org.apache.calcite.rel.core.AggregateCall; import org.apache.calcite.rel.core.Window; +import org.apache.calcite.rel.type.RelDataType; +import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexCall; import org.apache.calcite.rex.RexInputRef; @@ -106,6 +108,14 @@ private static RexNode toRexCall(RelBuilder builder, RexExpression.FunctionCall // Calcite creates IN as SEARCH return builder.in(operands.get(0), operands.subList(1, operands.size())); } + if (operator.kind == SqlKind.CAST) { + // The second operand only holds the target type name (see handleCast). Calcite would use its type as the result. + RexNode operand = operands.get(0); + RelDataTypeFactory typeFactory = builder.getTypeFactory(); + RelDataType type = typeFactory.createTypeWithNullability(rexExpression.getDataType().toType(typeFactory), + operand.getType().isNullable()); + return builder.getRexBuilder().makeCast(type, operand); + } return builder.call(operator, operands); } diff --git a/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/RexExpressionUtilsTest.java b/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/RexExpressionUtilsTest.java index 0d5355c36271..536b2ecb9483 100644 --- a/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/RexExpressionUtilsTest.java +++ b/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/RexExpressionUtilsTest.java @@ -28,6 +28,7 @@ import org.apache.calcite.rex.RexCall; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexLiteral; +import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexUnknownAs; import org.apache.calcite.sql.SqlCollation; import org.apache.calcite.sql.SqlKind; @@ -47,6 +48,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; /// Tests for RexExpressionUtils, focusing on the handleSearch method and null handling. @@ -92,6 +94,18 @@ public void testSqlKindFunctionNames() { assertSame(RexExpressionUtils.getAggFunction(sum0), SqlStdOperatorTable.SUM0); } + @Test + public void testCastRoundTrip() { + // Plan nodes hold the target type of CAST as a STRING literal operand. Field "a" is a nullable INTEGER. + RelBuilder relBuilder = + RelBuilder.create(Frameworks.newConfigBuilder().build()).values(new String[]{"a"}, 1, null); + RexExpression castCall = new RexExpression.FunctionCall(ColumnDataType.DOUBLE, SqlKind.CAST.name(), + List.of(new RexExpression.InputRef(0), new RexExpression.Literal(ColumnDataType.STRING, "DOUBLE"))); + RexNode rexNode = RexExpressionUtils.toRexNode(relBuilder, castCall); + assertTrue(rexNode.getType().isNullable()); + assertEquals(RexExpressionUtils.fromRexNode(rexNode), castCall); + } + @Test public void testHandleSearchNullLiteralInWithNullAsUnknown() { // Test: NULL IN (1, 2, 3) (when nullAs = UNKNOWN)