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 f707ef4b7e [CALCITE-7527] SqlParserUtil.parseTimestampTzLiteral does
not validate timezone
f707ef4b7e is described below
commit f707ef4b7eef4bbb75163d336a6cc1e72e7ce673
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu May 14 13:51:02 2026 -0700
[CALCITE-7527] SqlParserUtil.parseTimestampTzLiteral does not validate
timezone
Signed-off-by: Mihai Budiu <[email protected]>
---
.../apache/calcite/sql/parser/SqlParserUtil.java | 33 ++++++++++----
.../calcite/sql/parser/SqlParserUtilTest.java | 51 ++++++++++++++++++++++
.../org/apache/calcite/test/SqlValidatorTest.java | 2 +-
3 files changed, 76 insertions(+), 10 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index fac78417da..1ba1933fc7 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -65,6 +65,8 @@
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
+import java.time.DateTimeException;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.IllformedLocaleException;
@@ -403,21 +405,34 @@ public static SqlUuidLiteral parseUuidLiteral(String s,
SqlParserPos pos) {
public static SqlTimestampTzLiteral parseTimestampTzLiteral(
String s, SqlParserPos pos) {
- // We expect the string to end in a timezone.
- int lastSpace = s.lastIndexOf(" ");
- if (lastSpace >= 0) {
+ // We expect the string to to contain exactly two spaces:
+ // - one between date and time
+ // - one between time and timezone
+ long spaces = s.chars().filter(c -> c == ' ').count();
+ if (spaces == 2) {
+ int lastSpace = s.lastIndexOf(" ");
final String timeZone = s.substring(lastSpace + 1);
final String timestamp = s.substring(0, lastSpace);
- TimeZone tz = TimeZone.getTimeZone(timeZone);
- if (tz != null) {
- SqlTimestampLiteral ts = parseTimestampLiteral(SqlTypeName.TIMESTAMP,
timestamp, pos);
- TimestampWithTimeZoneString tsz = new
TimestampWithTimeZoneString(ts.getTimestamp(), tz);
- return SqlLiteral.createTimestamp(tsz, ts.getPrec(), pos);
+ try {
+ ZoneId zoneId = ZoneId.of(timeZone);
+ TimeZone tz = TimeZone.getTimeZone(zoneId);
+ if (tz != null) {
+ SqlTimestampLiteral ts =
parseTimestampLiteral(SqlTypeName.TIMESTAMP, timestamp, pos);
+ TimestampWithTimeZoneString tsz = new
TimestampWithTimeZoneString(ts.getTimestamp(), tz);
+ return SqlLiteral.createTimestamp(tsz, ts.getPrec(), pos);
+ }
+ } catch (DateTimeException e) {
+ String message = e.getMessage();
+ if (message == null) {
+ message = "Error parsing TIME ZONE";
+ }
+ throw SqlUtil.newContextException(pos,
+ RESOURCE.illegalLiteral("TIMESTAMP WITH TIME ZONE", s, message));
}
}
throw SqlUtil.newContextException(pos,
RESOURCE.illegalLiteral("TIMESTAMP WITH TIME ZONE", s,
- RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING).str()));
+ RESOURCE.badFormat(DateTimeUtils.TIMESTAMP_FORMAT_STRING + "
zone").str()));
}
private static SqlTimestampLiteral parseTimestampLiteral(SqlTypeName
typeName,
diff --git
a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
index ca1293c3dd..03de04f860 100644
--- a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/parser/SqlParserUtilTest.java
@@ -17,12 +17,17 @@
package org.apache.calcite.sql.parser;
import org.apache.calcite.avatica.util.TimeUnit;
+import org.apache.calcite.runtime.CalciteContextException;
import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlTimestampTzLiteral;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasToString;
+import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests {@link SqlParserUtil}. Currently, this test focuses on tests for the
methods that work
@@ -43,6 +48,52 @@ public class SqlParserUtilTest {
assertThat(SqlParserUtil.intervalToMillis("2", qualifier),
equalTo(120_000L));
}
+ @Test void testTimestampWithTimeZone() {
+ SqlParserPos pos = new SqlParserPos(2, 3);
+ SqlTimestampTzLiteral lit =
+ SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 GMT", pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10 GMT'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 UTC",
pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10 UTC'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10
America/Los_Angeles", pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10
America/Los_Angeles'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 +00:00",
pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10 UTC'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 Z", pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10 UTC'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 -00:30",
pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10
GMT-00:30'"));
+
+ lit = SqlParserUtil.parseTimestampTzLiteral("2020-01-01 10:10:10 +05:45",
pos);
+ assertThat(lit, hasToString("TIMESTAMP_TZ '2020-01-01 10:10:10
GMT+05:45'"));
+
+ // Test case for [CALCITE-7527] SqlParserUtil.parseTimestampTzLiteral does
not validate timezone
+ // https://issues.apache.org/jira/browse/CALCITE-7527
+ try {
+ SqlParserUtil.parseTimestampTzLiteral("2020-06-21
14:23:44.123654+00:00", pos);
+ fail("Should be unreachable");
+ } catch (CalciteContextException ex) {
+ assertThat(
+ ex.getMessage(), is("At line 2, column 3: Illegal TIMESTAMP WITH
TIME ZONE literal "
+ + "'2020-06-21 14:23:44.123654+00:00': not in format 'yyyy-MM-dd
HH:mm:ss zone'"));
+ }
+
+ try {
+ SqlParserUtil.parseTimestampTzLiteral("2020-06-21 14:23:44.123654
incorrect_zone", pos);
+ fail("Should be unreachable");
+ } catch (CalciteContextException ex) {
+ assertThat(
+ ex.getMessage(), is("At line 2, column 3: Illegal TIMESTAMP WITH
TIME ZONE literal "
+ + "'2020-06-21 14:23:44.123654 incorrect_zone': Unknown "
+ + "time-zone ID: incorrect_zone"));
+ }
+ }
+
@Test void testMinuteToSecondIntervalToMillis() {
final SqlIntervalQualifier qualifier =
new SqlIntervalQualifier(TimeUnit.MINUTE, TimeUnit.SECOND, POSITION);
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 36ef279b53..8a4a970e12 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -299,7 +299,7 @@ static SqlOperatorTable operatorTableFor(SqlLibrary
library) {
expr("^TIMESTAMP WITH LOCAL TIME ZONE '12-21-99, 12:30:00'^")
.fails("(?s).*Illegal TIMESTAMP WITH LOCAL TIME ZONE literal.*");
expr("^TIMESTAMP WITH TIME ZONE '12-21-99, 12:30:00'^")
- .fails("(?s).*Illegal TIMESTAMP literal.*");
+ .fails("(?s).*Illegal TIMESTAMP WITH TIME ZONE literal.*");
}
/**