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