You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by mb...@apache.org on 2026/10/18 06:55:56 UTC

(calcite) branch main updated: [CALCITE-7865] CASTS from string trim in inconsistent ways

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 2350b9016d [CALCITE-7865] CASTS from string trim in inconsistent ways
2350b9016d is described below

commit 2350b9016dfc16e0737b64ec5fe59b35da9e4fc8
Author: Mihai Budiu <mb...@feldera.com>
AuthorDate: Sat Oct 17 17:33:53 2026 -0700

    [CALCITE-7865] CASTS from string trim in inconsistent ways
    
    Signed-off-by: Mihai Budiu <mb...@feldera.com>
---
 .../calcite/adapter/enumerable/EnumUtils.java      |  1 +
 .../org/apache/calcite/runtime/SqlFunctions.java   | 18 ++++++---
 .../org/apache/calcite/util/BuiltInMethod.java     |  2 +-
 .../apache/calcite/linq4j/tree/UnsignedType.java   |  8 ++--
 .../org/apache/calcite/test/SqlOperatorTest.java   | 44 ++++++++++++++++++++++
 5 files changed, 63 insertions(+), 10 deletions(-)

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 0aaf1f64fe..3711de36b8 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
@@ -502,6 +502,7 @@ public static Expression convert(Expression operand, Type fromType,
       if (toPrimitive != null) {
         switch (toPrimitive) {
         case CHAR:
+        case BYTE:
         case SHORT:
         case INT:
         case LONG:
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 2b3a45c479..4dceed1679 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -5285,7 +5285,7 @@ private static Object cannotConvert(Object o, Class toType) {
 
   /** CAST(VARCHAR AS BOOLEAN). */
   public static boolean toBoolean(String s) {
-    s = trim(true, true, " ", s);
+    s = s.trim();
     if (s.equalsIgnoreCase("TRUE")) {
       return true;
     } else if (s.equalsIgnoreCase("FALSE")) {
@@ -5321,12 +5321,14 @@ public static boolean toBoolean(Object o) {
         : (Boolean) cannotConvert(o, boolean.class);
   }
 
-  // Don't need parseByte etc. - Byte.parseByte is sufficient.
+  public static byte toByte(String s) {
+    return parseByte(s.trim());
+  }
 
   public static byte toByte(Object o) {
     return o instanceof Byte ? (Byte) o
         : o instanceof Number ? toByte((Number) o)
-        : parseByte(o.toString());
+        : toByte(o.toString());
   }
 
   public static byte toByte(Number number) {
@@ -5698,11 +5700,17 @@ public static java.sql.Time internalToTime(int v) {
     return v == null ? castNonNull(null) : internalToTime(v.intValue());
   }
 
+  /** CAST(VARCHAR AS DATE). */
+  public static int stringToDate(String s) {
+    // Trim here, because Avatica rejects some characters below U+0020
+    return DateTimeUtils.dateStringToUnixDate(s.trim());
+  }
+
   public static @PolyNull Integer toTimeWithLocalTimeZone(@PolyNull String v) {
     if (v == null) {
       return castNonNull(null);
     }
-    return new TimeWithTimeZoneString(v)
+    return new TimeWithTimeZoneString(v.trim())
         .withTimeZone(DateTimeUtils.UTC_ZONE)
         .getLocalTimeString()
         .getMillisOfDay();
@@ -6360,7 +6368,7 @@ public static int time(long timestampMillis, String timeZone) {
     if (v == null) {
       return castNonNull(null);
     }
-    return new TimestampWithTimeZoneString(v)
+    return new TimestampWithTimeZoneString(v.trim())
         .withTimeZone(DateTimeUtils.UTC_ZONE)
         .getLocalTimestampString()
         .getMillisSinceEpoch();
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 6aba599523..5fd9d9a715 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -735,7 +735,7 @@ public enum BuiltInMethod {
   INTERNAL_TO_DATE(SqlFunctions.class, "internalToDate", int.class),
   INTERNAL_TO_TIME(SqlFunctions.class, "internalToTime", int.class),
   INTERNAL_TO_TIMESTAMP(SqlFunctions.class, "internalToTimestamp", long.class),
-  STRING_TO_DATE(DateTimeUtils.class, "dateStringToUnixDate", String.class),
+  STRING_TO_DATE(SqlFunctions.class, "stringToDate", String.class),
   STRING_TO_TIME(DateTimeUtils.class, "timeStringToUnixDate", String.class),
   STRING_TO_TIMESTAMP(DateTimeUtils.class, "timestampStringToUnixDate", String.class),
   STRING_TO_TIME_WITH_LOCAL_TIME_ZONE(SqlFunctions.class, "toTimeWithLocalTimeZone",
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 32622315f7..ae3d37ebd3 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);
+    return Unsigned.ubyte(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);
+    return Unsigned.ushort(s.trim());
   }
 
   // UInteger
@@ -246,7 +246,7 @@ private static UInteger toUIntegerNonNull(Number n, RoundingMode mode) {
     if (s == null) {
       return null;
     }
-    return Unsigned.uint(s);
+    return Unsigned.uint(s.trim());
   }
 
   // ULong
@@ -304,7 +304,7 @@ private static ULong toULongNonNull(Number n, RoundingMode mode) {
     if (s == null) {
       return null;
     }
-    return Unsigned.ulong(s);
+    return Unsigned.ulong(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 53d093144b..6895a4098b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -1894,6 +1894,50 @@ void testCastToBoolean(CastType castType, SqlOperatorFixture f) {
     }
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-7865">[CALCITE-7865]
+   * CASTS from string trim in inconsistent ways</a>. */
+  @ParameterizedTest
+  @MethodSource("safeParameters")
+  void testCastFromStringTrim(CastType castType, SqlOperatorFixture f) {
+    f.setFor(SqlStdOperatorTable.CAST, VmName.EXPAND);
+    // Target type, and a valid string for that type
+    final String[][] casts = {
+        {"TINYINT", "1"},
+        {"SMALLINT", "1"},
+        {"INTEGER", "1"},
+        {"BIGINT", "1"},
+        {"TINYINT UNSIGNED", "1"},
+        {"SMALLINT UNSIGNED", "1"},
+        {"INTEGER UNSIGNED", "1"},
+        {"BIGINT UNSIGNED", "1"},
+        {"REAL", "1.5"},
+        {"DOUBLE", "1.5"},
+        {"DECIMAL(5, 2)", "1.5"},
+        {"BOOLEAN", "true"},
+        {"DATE", "2020-01-02"},
+        {"TIME", "10:11:12"},
+        {"TIMESTAMP", "2020-01-02 10:11:12"},
+        {"TIME WITH LOCAL TIME ZONE", "10:11:12 UTC"},
+        {"TIMESTAMP WITH LOCAL TIME ZONE", "2020-01-02 10:11:12 UTC"},
+        {"INTERVAL DAY", "3"},
+    };
+    for (String[] cast : casts) {
+      final String type = cast[0];
+      final String value = cast[1];
+      // Each cast removes leading and trailing characters up to U+0020,
+      // as String.trim does
+      for (String c : new String[] {"\\0009", "\\000A", "\\001F", "\\0020"}) {
+        f.checkBoolean("cast(U&'" + c + value + c + "' as " + type + ")"
+            + " = cast('" + value + "' as " + type + ")", true);
+      }
+      if (castType != CastType.CAST) {
+        // No cast removes U+00A0 (no-break space), which is above U+0020
+        f.checkNull("cast(U&'\\00A0" + value + "' as " + type + ")");
+      }
+    }
+  }
+
   @Test void testCastToDecimal() {
     final SqlOperatorFixture f = fixture();
     // test the minimum scale is 0