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

Reply via email to