This is an automated email from the ASF dual-hosted git repository.

mbudiu 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 4ee33c9488 [CALCITE-7001] Cast of malformed literal to TIMESTAMP WITH 
LOCAL TIME ZONE need to throw informative error
4ee33c9488 is described below

commit 4ee33c9488740e41311c51735538dbac9e5abc4d
Author: Evgeniy Stanilovsky <[email protected]>
AuthorDate: Tue May 6 19:25:26 2025 +0300

    [CALCITE-7001] Cast of malformed literal to TIMESTAMP WITH LOCAL TIME ZONE 
need to throw informative error
---
 .../java/org/apache/calcite/rex/RexBuilder.java    |  4 +-
 .../calcite/util/TimestampWithTimeZoneString.java  | 61 +++++++++++++++-------
 .../org/apache/calcite/rex/RexBuilderTest.java     |  4 +-
 .../org/apache/calcite/test/SqlFunctionsTest.java  | 40 ++++++++++++++
 core/src/test/resources/sql/misc.iq                | 18 +++++++
 5 files changed, 104 insertions(+), 23 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java 
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index d2729da71f..6f258ed267 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1976,9 +1976,9 @@ private static Comparable zeroValue(RelDataType type) {
     case TIME_TZ:
       return new TimeWithTimeZoneString(0, 0, 0, "GMT+00");
     case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
-      return new TimestampString(0, 1, 1, 0, 0, 0);
+      return new TimestampString(1, 1, 1, 0, 0, 0);
     case TIMESTAMP_TZ:
-      return new TimestampWithTimeZoneString(0, 1, 1, 0, 0, 0, "GMT+00");
+      return new TimestampWithTimeZoneString(1, 1, 1, 0, 0, 0, "GMT+00");
     default:
       throw Util.unexpected(type.getSqlTypeName());
     }
diff --git 
a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java 
b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
index e3c08bb7f1..14195469ab 100644
--- 
a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
+++ 
b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java
@@ -22,12 +22,17 @@
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Locale;
 import java.util.TimeZone;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import static org.apache.calcite.avatica.util.DateTimeUtils.DATE_FORMAT_STRING;
+import static 
org.apache.calcite.avatica.util.DateTimeUtils.TIMESTAMP_FORMAT_STRING;
+import static org.apache.calcite.util.DateTimeStringUtils.getDateFormatter;
+import static org.apache.calcite.util.Static.RESOURCE;
+
 import static java.lang.Math.floorMod;
+import static java.util.Objects.requireNonNull;
 
 /**
  * Timestamp with time-zone literal.
@@ -41,21 +46,39 @@ public class TimestampWithTimeZoneString
   final TimestampString localDateTime;
   final TimeZone timeZone;
   final String v;
+  private final DateTimeUtils.PrecisionTime pt;
+
+  private static final ThreadLocal<@Nullable SimpleDateFormat> 
TIMESTAMP_FORMAT =
+      ThreadLocal.withInitial(() ->
+          getDateFormatter(TIMESTAMP_FORMAT_STRING));
 
   /** Creates a TimestampWithTimeZoneString. */
   public TimestampWithTimeZoneString(TimestampString localDateTime, TimeZone 
timeZone) {
     this.localDateTime = localDateTime;
     this.timeZone = timeZone;
     this.v = localDateTime.toString() + " " + timeZone.getID();
+
+    this.pt = parseDateTime(localDateTime.toString(), this.timeZone);
   }
 
   /** Creates a TimestampWithTimeZoneString. */
   public TimestampWithTimeZoneString(String v) {
-    this.localDateTime = new TimestampString(v.substring(0, v.indexOf(' ', 
11)));
-    String timeZoneString = v.substring(v.indexOf(' ', 11) + 1);
+    // search next space after "yyyy-MM-dd "
+    int pos = v.indexOf(' ', DATE_FORMAT_STRING.length() + 1);
+
+    if (pos == -1) {
+      throw RESOURCE.illegalLiteral("TIMESTAMP WITH LOCAL TIME ZONE", v,
+              RESOURCE.badFormat(TIMESTAMP_FORMAT_STRING).str()).ex();
+    }
+
+    String tsStr = v.substring(0, pos);
+    this.localDateTime = new TimestampString(tsStr);
+
+    String timeZoneString = v.substring(v.indexOf(' ', 
DATE_FORMAT_STRING.length() + 1) + 1);
     checkArgument(DateTimeStringUtils.isValidTimeZone(timeZoneString));
     this.timeZone = TimeZone.getTimeZone(timeZoneString);
     this.v = v;
+    this.pt = parseDateTime(tsStr, this.timeZone);
   }
 
   /** Creates a TimestampWithTimeZoneString for year, month, day, hour, minute,
@@ -110,23 +133,10 @@ public TimestampWithTimeZoneString withTimeZone(TimeZone 
timeZone) {
     if (this.timeZone.equals(timeZone)) {
       return this;
     }
-    String localDateTimeString = localDateTime.toString();
-    String v;
-    String fraction;
-    int i = localDateTimeString.indexOf('.');
-    if (i >= 0) {
-      v = localDateTimeString.substring(0, i);
-      fraction = localDateTimeString.substring(i + 1);
-    } else {
-      v = localDateTimeString;
-      fraction = null;
-    }
-    final DateTimeUtils.PrecisionTime pt =
-        DateTimeUtils.parsePrecisionDateTimeLiteral(v,
-            new SimpleDateFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING, 
Locale.ROOT),
-            this.timeZone, -1);
+    String fraction = pt.getFraction();
+
     pt.getCalendar().setTimeZone(timeZone);
-    if (fraction != null) {
+    if (!fraction.isEmpty()) {
       return new TimestampWithTimeZoneString(
           pt.getCalendar().get(Calendar.YEAR),
           pt.getCalendar().get(Calendar.MONTH) + 1,
@@ -202,4 +212,17 @@ public TimestampString getLocalTimestampString() {
   public TimeZone getTimeZone() {
     return timeZone;
   }
+
+  private static DateTimeUtils.PrecisionTime parseDateTime(String tsStr, 
TimeZone timeZone) {
+    DateTimeUtils.PrecisionTime pt =
+        DateTimeUtils.parsePrecisionDateTimeLiteral(tsStr, 
requireNonNull(TIMESTAMP_FORMAT.get()),
+            timeZone, -1);
+
+    if (pt == null) {
+      throw RESOURCE.illegalLiteral("TIMESTAMP WITH LOCAL TIME ZONE", tsStr,
+          RESOURCE.badFormat(TIMESTAMP_FORMAT_STRING).str()).ex();
+    }
+
+    return pt;
+  }
 }
diff --git a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java 
b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
index 76e35c18ed..4b714e8632 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
@@ -1121,11 +1121,11 @@ private static Stream<Arguments> 
testData4testMakeZeroLiteral() {
         
type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIME_WITH_LOCAL_TIME_ZONE),
             relDataType -> new TimeString(0, 0, 0)),
         
type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE),
-            relDataType -> new TimestampString(0, 1, 1, 0, 0, 0)),
+            relDataType -> new TimestampString(1, 1, 1, 0, 0, 0)),
         type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIME_TZ),
             relDataType -> new TimeWithTimeZoneString(0, 0, 0, "GMT+00:00")),
         
type2rexLiteral.apply(typeFactory.createSqlType(SqlTypeName.TIMESTAMP_TZ),
-            relDataType -> new TimestampWithTimeZoneString(0, 1, 1, 0, 0, 0, 
"GMT+00:00")));
+            relDataType -> new TimestampWithTimeZoneString(1, 1, 1, 0, 0, 0, 
"GMT+00:00")));
   }
 
   /** Test case for
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index d68d4b2988..d1f8cb9322 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -74,10 +74,12 @@
 import static org.apache.calcite.runtime.SqlFunctions.toIntOptional;
 import static org.apache.calcite.runtime.SqlFunctions.toLong;
 import static org.apache.calcite.runtime.SqlFunctions.toLongOptional;
+import static 
org.apache.calcite.runtime.SqlFunctions.toTimestampWithLocalTimeZone;
 import static org.apache.calcite.runtime.SqlFunctions.trim;
 import static org.apache.calcite.runtime.SqlFunctions.upper;
 import static org.apache.calcite.test.Matchers.isListOf;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
@@ -1836,6 +1838,44 @@ private void thereAndBack(byte[] bytes) {
     assertThat(toLongOptional(null), is(nullValue()));
   }
 
+  /**
+   * Test date after 0001-01-01 required by ANSI SQL - is passed.
+   * Test date before 0001-01-01 and malformed date time literal - is failed.
+   */
+  @Test void testToTimestampWithLocalTimeZone() {
+    Long ret = toTimestampWithLocalTimeZone("1970-01-01 00:00:01", 
TimeZone.getTimeZone("UTC"));
+    assertThat(ret, is(1000L));
+
+    ret = toTimestampWithLocalTimeZone("1970-01-01 00:00:01.010", 
TimeZone.getTimeZone("UTC"));
+    assertThat(ret, is(1010L));
+
+    ret = toTimestampWithLocalTimeZone("1970-01-01 00:00:01 "
+        + TimeZone.getTimeZone("UTC").getID());
+    assertThat(ret, is(1000L));
+
+    // exceptional scenarios
+    try {
+      ret = toTimestampWithLocalTimeZone("malformed", TimeZone.getDefault());
+      fail("expected error, got " + ret);
+    } catch (CalciteException e) {
+      assertThat(e.getMessage(), containsString("Illegal TIMESTAMP WITH LOCAL 
TIME ZONE literal"));
+    }
+
+    try {
+      ret = toTimestampWithLocalTimeZone("0000-01-01 00:00:01", 
TimeZone.getDefault());
+      fail("expected error, got " + ret);
+    } catch (CalciteException e) {
+      assertThat(e.getMessage(), containsString("Illegal TIMESTAMP WITH LOCAL 
TIME ZONE literal"));
+    }
+
+    try {
+      ret = toTimestampWithLocalTimeZone("malformed " + 
TimeZone.getDefault().getID());
+      fail("expected error, got " + ret);
+    } catch (CalciteException e) {
+      assertThat(e.getMessage(), containsString("Illegal TIMESTAMP WITH LOCAL 
TIME ZONE literal"));
+    }
+  }
+
   /**
    * Tests that a nullable timestamp in the given time zone converts to a Unix
    * timestamp in UTC.
diff --git a/core/src/test/resources/sql/misc.iq 
b/core/src/test/resources/sql/misc.iq
index 74c44a9798..073d9b4803 100644
--- a/core/src/test/resources/sql/misc.iq
+++ b/core/src/test/resources/sql/misc.iq
@@ -2665,4 +2665,22 @@ group by z;
 
 !ok
 
+# [CALCITE-7001] Cast of malformed literal to TIMESTAMP WITH LOCAL TIME ZONE
+select cast ('malformed' AS TIMESTAMP WITH LOCAL TIME ZONE);
+Illegal TIMESTAMP WITH LOCAL TIME ZONE literal 'malformed'
+!error
+
+# [CALCITE-7001] Cast of malformed literal to TIMESTAMP WITH LOCAL TIME ZONE
+select cast ('0000-01-01 00:00:00' AS TIMESTAMP WITH LOCAL TIME ZONE);
+Illegal TIMESTAMP WITH LOCAL TIME ZONE literal '0000-01-01 00:00:00'
+!error
+
+select TIMESTAMP WITH LOCAL TIME ZONE 'malformed';
+From line 1, column 8 to line 1, column 49: Illegal TIMESTAMP WITH LOCAL TIME 
ZONE literal 'malformed'
+!error
+
+select TIMESTAMP WITH LOCAL TIME ZONE '0000-01-01 00:00:00';
+From line 1, column 8 to line 1, column 59: Illegal TIMESTAMP WITH LOCAL TIME 
ZONE literal '0000-01-01 00:00:00'
+!error
+
 # End misc.iq

Reply via email to