This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 8c3770b22d [CALCITE-7866] CASTS from string values accept Java idioms
which are not legal in SQL
8c3770b22d is described below
commit 8c3770b22dfeab8f3a22e0352ba030fd3eb78382
Author: Mihai Budiu <[email protected]>
AuthorDate: Sun Oct 18 00:17:38 2026 -0700
[CALCITE-7866] CASTS from string values accept Java idioms which are not
legal in SQL
Signed-off-by: Mihai Budiu <[email protected]>
---
.../org/apache/calcite/runtime/SqlFunctions.java | 39 +++++++++++++----
.../org/apache/calcite/linq4j/tree/Primitive.java | 32 +++++++++++++-
.../apache/calcite/linq4j/tree/UnsignedType.java | 8 ++--
.../org/apache/calcite/test/SqlOperatorTest.java | 50 ++++++++++++++++++++++
4 files changed, 115 insertions(+), 14 deletions(-)
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 4dceed1679..1c7159dedd 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -5286,15 +5286,36 @@ private static Object cannotConvert(Object o, Class
toType) {
/** CAST(VARCHAR AS BOOLEAN). */
public static boolean toBoolean(String s) {
s = s.trim();
- if (s.equalsIgnoreCase("TRUE")) {
+ if (equalsIgnoreAsciiCase(s, "TRUE")) {
return true;
- } else if (s.equalsIgnoreCase("FALSE")) {
+ } else if (equalsIgnoreAsciiCase(s, "FALSE")) {
return false;
} else {
throw RESOURCE.invalidCharacterForCast(s).ex();
}
}
+ /** Returns 'true' is s equals and upper-case word 'upper', ignoring
+ * only the case of ASCII characters. */
+ private static boolean equalsIgnoreAsciiCase(String s, String upper) {
+ if (s.length() != upper.length()) {
+ return false;
+ }
+ // We restrict the comparison to ASCII characters: for
+ // example, U+017F (long s) upper-cased is 'S', but we do
+ // not want to match it.
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c >= 'a' && c <= 'z') {
+ c = (char) (c - 'a' + 'A');
+ }
+ if (c != upper.charAt(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
public static boolean toBoolean(Number number) {
if (number instanceof BigDecimal) {
BigDecimal decimal = (BigDecimal) number;
@@ -5322,7 +5343,7 @@ public static boolean toBoolean(Object o) {
}
public static byte toByte(String s) {
- return parseByte(s.trim());
+ return parseByte(Primitive.checkNumericString(s.trim()));
}
public static byte toByte(Object o) {
@@ -5344,7 +5365,7 @@ public static Character toCharBoxed(String s) {
}
public static short toShort(String s) {
- return parseShort(s.trim());
+ return parseShort(Primitive.checkNumericString(s.trim()));
}
public static short toShort(Number number) {
@@ -5453,7 +5474,7 @@ public static int toInt(java.sql.Time v) {
}
public static int toInt(String s) {
- return parseInt(s.trim());
+ return parseInt(Primitive.checkNumericString(s.trim()));
}
public static int toInt(Number number) {
@@ -5557,7 +5578,7 @@ public static long toLong(String s) {
if (s.startsWith("199") && s.contains(":")) {
return Timestamp.valueOf(s).getTime();
}
- return parseLong(s.trim());
+ return parseLong(Primitive.checkNumericString(s.trim()));
}
public static long toLong(Number number) {
@@ -5580,7 +5601,7 @@ public static long toLong(Object o) {
}
public static float toFloat(String s) {
- return parseFloat(s.trim());
+ return parseFloat(Primitive.checkApproxNumericString(s.trim()));
}
public static float toFloat(Number number) {
@@ -5595,7 +5616,7 @@ public static float toFloat(Object o) {
}
public static double toDouble(String s) {
- return parseDouble(s.trim());
+ return parseDouble(Primitive.checkApproxNumericString(s.trim()));
}
public static double toDouble(Number number) {
@@ -5615,7 +5636,7 @@ public static BigDecimal toBigDecimal(String s) {
"Cannot convert null string to BigDecimal");
}
try {
- return new BigDecimal(s.trim());
+ return new BigDecimal(Primitive.checkNumericString(s.trim()));
} catch (NumberFormatException e) {
NumberFormatException ex =
new NumberFormatException("Invalid value for BigDecimal: \"" + s +
"\"");
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java
index 371685cd40..770f067b1c 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/Primitive.java
@@ -446,10 +446,40 @@ private static boolean isBoundedDecimal(BigDecimal value)
{
if (value == null) {
return null;
}
- BigDecimal result = new BigDecimal(value.trim());
+ BigDecimal result = new BigDecimal(checkNumericString(value.trim()));
return checkOverflow(result, precision, scale, roundingMode);
}
+ /** Returns a string unchanged if each of its characters can occur in a SQL
+ * numeric literal, otherwise throws.
+ *
+ * <p>This function is more strict than e.g., Double.parseDouble
+ * which accepts "1.5d" and "0x1p3". */
+ public static String checkNumericString(String s) {
+ for (int i = 0; i < s.length(); i++) {
+ final char c = s.charAt(i);
+ if ((c < '0' || c > '9')
+ && c != '+' && c != '-' && c != '.' && c != 'e' && c != 'E') {
+ throw new NumberFormatException("For input string: \"" + s + "\"");
+ }
+ }
+ return s;
+ }
+
+ /** An extension of {@link Primitive#checkNumericString} which also
+ * accepts special floating-point values. */
+ public static String checkApproxNumericString(String s) {
+ switch (s) {
+ case "NaN":
+ case "Infinity":
+ case "+Infinity":
+ case "-Infinity":
+ return s;
+ default:
+ return checkNumericString(s);
+ }
+ }
+
/**
* Convert a short time interval to a decimal value.
* Called from BuiltInMethod.SHORT_INTERVAL_DECIMAL_CAST.
diff --git
a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java
index ae3d37ebd3..5f00d9dfc7 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnsignedType.java
@@ -134,7 +134,7 @@ private static UByte toUByteNonNull(Number n, RoundingMode
mode) {
if (s == null) {
return null;
}
- return Unsigned.ubyte(s.trim());
+ return Unsigned.ubyte(Primitive.checkNumericString(s.trim()));
}
// To UShort
@@ -192,7 +192,7 @@ private static UShort toUShortNonNull(Number n,
RoundingMode mode) {
if (s == null) {
return null;
}
- return Unsigned.ushort(s.trim());
+ return Unsigned.ushort(Primitive.checkNumericString(s.trim()));
}
// UInteger
@@ -246,7 +246,7 @@ private static UInteger toUIntegerNonNull(Number n,
RoundingMode mode) {
if (s == null) {
return null;
}
- return Unsigned.uint(s.trim());
+ return Unsigned.uint(Primitive.checkNumericString(s.trim()));
}
// ULong
@@ -304,7 +304,7 @@ private static ULong toULongNonNull(Number n, RoundingMode
mode) {
if (s == null) {
return null;
}
- return Unsigned.ulong(s.trim());
+ return Unsigned.ulong(Primitive.checkNumericString(s.trim()));
}
public static @Nullable UShort toUShort(Object o) {
if (o == null) {
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 6895a4098b..7453fecc40 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -1938,6 +1938,56 @@ void testCastFromStringTrim(CastType castType,
SqlOperatorFixture f) {
}
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7866">[CALCITE-7866]
+ * CASTS from string values accept Java idioms which are not legal in
+ * SQL</a>. */
+ @ParameterizedTest
+ @MethodSource("safeParameters")
+ void testCastFromStringJavaIdiom(CastType castType, SqlOperatorFixture f) {
+ f.setFor(SqlStdOperatorTable.CAST, VmName.EXPAND);
+ // Negative tests: casts should fail
+ // A string that is legal in Java but not in SQL, and the target type
+ final String[][] casts = {
+ // Type suffix on a Java number
+ {"1.5d", "DOUBLE"},
+ {"1.5f", "REAL"},
+ // Hexadecimal floating-point number
+ {"0x1p3", "DOUBLE"},
+ // Character.digit accepts U+0667, ARABIC-INDIC DIGIT SEVEN
+ {"\\0667", "TINYINT"},
+ {"\\0667", "SMALLINT"},
+ {"\\0667", "INTEGER"},
+ {"\\0667", "BIGINT"},
+ {"\\0667", "INTEGER UNSIGNED"},
+ {"\\0667", "BIGINT UNSIGNED"},
+ {"\\0667", "DECIMAL(5, 2)"},
+ {"\\0667", "INTERVAL DAY"},
+ // equalsIgnoreCase upper-cases U+017F, LATIN SMALL LETTER LONG S, to S
+ {"fal\\017Fe", "BOOLEAN"},
+ };
+ for (String[] cast : casts) {
+ final String value = cast[0];
+ final String type = cast[1];
+ final String expression = "cast(U&'" + value + "' as " + type + ")";
+ if (castType == CastType.CAST) {
+ f.checkFails(expression,
+ type.equals("BOOLEAN")
+ ? "Invalid character for cast.*"
+ : WRONG_FORMAT_MESSAGE,
+ true);
+ } else {
+ f.checkNull(expression);
+ }
+ }
+
+ // Each part of the SQL syntax for a number still converts
+ f.checkBoolean("cast('+1.5E+1' as double) = 15", true);
+ f.checkBoolean("cast('-.25e1' as real) = -2.5e0", true);
+ f.checkBoolean("cast('5.' as decimal(5, 2)) = 5", true);
+ f.checkBoolean("cast('+7' as tinyint) = 7", true);
+ }
+
@Test void testCastToDecimal() {
final SqlOperatorFixture f = fixture();
// test the minimum scale is 0