diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java index 9cd7ba802bd..fd60a29ae75 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumUtils.java @@ -539,6 +539,15 @@ public static Expression convert(Expression operand, Type fromType, } } } + if (toType == Number.class + && (fromType == Object.class || fromType == String.class)) { + // E.g. from "Object" to "Number". + // Generate "x == null ? null : SqlFunctions.toBigDecimal(x)". + return Expressions.condition( + Expressions.equal(operand, RexImpTable.NULL_EXPR), + RexImpTable.NULL_EXPR, + Expressions.call(SqlFunctions.class, "toBigDecimal", operand)); + } if (toPrimitive != null) { if (fromPrimitive != null) { // E.g. from "float" to "double" diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java index b797264a8bb..4e2f0af448f 100644 --- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java +++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java @@ -5522,7 +5522,18 @@ public static double toDouble(Object o) { } public static BigDecimal toBigDecimal(String s) { - return new BigDecimal(s.trim()); + if (s == null) { + throw new NumberFormatException( + "Cannot convert null string to BigDecimal"); + } + try { + return new BigDecimal(s.trim()); + } catch (NumberFormatException e) { + NumberFormatException ex = + new NumberFormatException("Invalid value for BigDecimal: \"" + s + "\""); + ex.initCause(e); + throw ex; + } } public static BigDecimal toBigDecimal(Number number) { @@ -5535,6 +5546,9 @@ public static BigDecimal toBigDecimal(Number number) { } public static BigDecimal toBigDecimal(Object o) { + if (o == null) { + throw new NumberFormatException("Cannot convert null to BigDecimal"); + } return o instanceof Number ? toBigDecimal((Number) o) : toBigDecimal(o.toString()); } diff --git a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java index 70370d7bb1a..d92f50acfae 100644 --- a/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java +++ b/core/src/test/java/org/apache/calcite/adapter/enumerable/EnumUtilsTest.java @@ -41,6 +41,29 @@ */ public final class EnumUtilsTest { + /** Test case for + * [CALCITE-6284] + * Invalid conversion triggers ClassCastException. */ + @Test void testObjectToNumberConvert() { + // Object x; + final ParameterExpression objectVariable = + Expressions.parameter(0, Object.class, "x"); + final Expression objectToNumber = + EnumUtils.convert(objectVariable, Number.class); + assertThat(Expressions.toString(objectToNumber), + is("x == null ? (java.math.BigDecimal) null" + + " : org.apache.calcite.runtime.SqlFunctions.toBigDecimal(x)")); + + // String s; + final ParameterExpression stringVariable = + Expressions.parameter(0, String.class, "s"); + final Expression stringToNumber = + EnumUtils.convert(stringVariable, Number.class); + assertThat(Expressions.toString(stringToNumber), + is("s == null ? (java.math.BigDecimal) null" + + " : org.apache.calcite.runtime.SqlFunctions.toBigDecimal(s)")); + } + @Test void testDateTypeToInnerTypeConvert() { // java.sql.Date x; final ParameterExpression date = diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java index d235fa327c7..c74d0d39b83 100644 --- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java +++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java @@ -9695,6 +9695,49 @@ void checkCalciteSchemaGetSubSchemaMap(boolean cache) { } } + /** Test case for + * [CALCITE-6284] + * Invalid conversion triggers ClassCastException. */ + @Test void bindStringParameter() { + for (SqlTypeName tpe : SqlTypeName.INT_TYPES) { + final String sql = + "with cte as (select cast(100 as " + tpe.getName() + ") as empid)" + + "select * from cte where empid = ?"; + + CalciteAssert.hr() + .query(sql) + .consumesPreparedStatement(p -> { + p.setString(1, "100"); + }) + .returnsUnordered("EMPID=100"); + } + } + + @Test void bindInvalidStringParameter() { + for (SqlTypeName tpe : SqlTypeName.INT_TYPES) { + final String sql = + "with cte as (select cast(100 as " + tpe.getName() + ") as empid)" + + "select * from cte where empid = ?"; + + final SQLException e = + assertThrows(SQLException.class, + () -> CalciteAssert.hr() + .query(sql) + .consumesPreparedStatement(p -> { + p.setString(1, "abc"); + }) + .returnsUnordered("")); + // Should produce a meaningful error, not ClassCastException + final Throwable cause = e.getCause(); + assertThat("Expected NumberFormatException for tpe=" + tpe, + cause, instanceOf(NumberFormatException.class)); + assertThat("Error message should contain the invalid value", + cause.getMessage(), containsString("abc")); + assertThat("Original NumberFormatException should be preserved as cause", + cause.getCause(), instanceOf(NumberFormatException.class)); + } + } + @Test void bindShortParameter() { for (SqlTypeName tpe : SqlTypeName.INT_TYPES) { final String sql =