From eb7c4fdc078cbc471064d06ac4faf21186d2faee Mon Sep 17 00:00:00 2001 From: Mihai Budiu Date: Mon, 27 Apr 2026 21:57:21 -0700 Subject: [PATCH] [CALCITE-7491] Literals of type TIMESTAMP WITH TIME ZONE cause crashes Signed-off-by: Mihai Budiu --- babel/src/test/resources/sql/big-query.iq | 37 +++++++++++++++++++ .../enumerable/RexToLixTranslator.java | 2 + .../org/apache/calcite/rex/RexLiteral.java | 18 ++++++++- .../sql2rel/SqlNodeToRexConverterImpl.java | 3 +- .../util/TimestampWithTimeZoneString.java | 2 +- site/_docs/reference.md | 1 + 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/babel/src/test/resources/sql/big-query.iq b/babel/src/test/resources/sql/big-query.iq index 007e68cb870c..a988eac1d74c 100755 --- a/babel/src/test/resources/sql/big-query.iq +++ b/babel/src/test/resources/sql/big-query.iq @@ -30,6 +30,43 @@ !use scott-big-query !set outputformat mysql +# Test case for [CALCITE-7491] https://issues.apache.org/jira/browse/CALCITE-7491 +# Literals of type TIMESTAMP WITH TIME ZONE cause crashes +# This will change once we fix [CALCITE-7494] +# Avatica conversion to string of TIMESTAMP WITH TIME ZONE +# does not include time zone +select TIMESTAMP WITH TIME ZONE '2020-01-01 00:00:00 America/New_York'; ++---------------------+ +| EXPR$0 | ++---------------------+ +| 2020-01-01 05:00:00 | ++---------------------+ +(1 row) + +!ok + +# Two timestamps with time zone are equal if they represent the same UTC time +SELECT TIMESTAMP WITH TIME ZONE '2020-01-01 08:10:10 America/New_York' = TIMESTAMP WITH TIME ZONE '2020-01-01 05:10:10 America/Los_Angeles'; ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + +# Two equal timestamps in different time zones are different if they represent different UTC times +SELECT TIMESTAMP WITH TIME ZONE '2020-01-01 08:10:10 America/New_York' = TIMESTAMP WITH TIME ZONE '2020-01-01 08:10:10 America/Los_Angeles'; ++--------+ +| EXPR$0 | ++--------+ +| false | ++--------+ +(1 row) + +!ok + # Two tests for [CALCITE-7094] Using a type alias as a constructor function # causes a validator assertion failure select int64(); diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java index f88170b1b58d..61fa2b0d7163 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java @@ -1077,6 +1077,7 @@ public static Expression translateLiteral( () -> "value for " + literal).toString())); case DATE: case TIME: + case TIME_TZ: case TIME_WITH_LOCAL_TIME_ZONE: case INTERVAL_YEAR: case INTERVAL_YEAR_MONTH: @@ -1085,6 +1086,7 @@ public static Expression translateLiteral( javaClass = int.class; break; case TIMESTAMP: + case TIMESTAMP_TZ: case TIMESTAMP_WITH_LOCAL_TIME_ZONE: case INTERVAL_DAY: case INTERVAL_DAY_HOUR: diff --git a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java index 7fd05901e74f..545e81995d0d 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexLiteral.java +++ b/core/src/main/java/org/apache/calcite/rex/RexLiteral.java @@ -60,6 +60,11 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.List; import java.util.Locale; @@ -1183,9 +1188,18 @@ public boolean isNull() { break; case TIMESTAMP_TZ: if (clazz == Long.class) { - return clazz.cast(((TimestampWithTimeZoneString) value) + TimestampWithTimeZoneString tstz = (TimestampWithTimeZoneString) value; + long ms = tstz .getLocalTimestampString() - .getMillisSinceEpoch()); + .getMillisSinceEpoch(); + // Interpret the timestamp part as a UTC timestamp + LocalDateTime local = Instant.ofEpochMilli(ms).atZone(ZoneOffset.UTC).toLocalDateTime(); + // Adjust for the time zone + ZoneId id = tstz.getTimeZone().toZoneId(); + ZonedDateTime zoned = local.atZone(id); + ZonedDateTime utc = zoned.withZoneSameInstant(ZoneOffset.UTC); + ms = utc.toInstant().toEpochMilli(); + return clazz.cast(ms); } else if (clazz == Calendar.class) { TimestampWithTimeZoneString ts = (TimestampWithTimeZoneString) value; return clazz.cast(ts.getLocalTimestampString().toCalendar(ts.getTimeZone())); diff --git a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java index ff0d1dfa74ab..081a81cbc096 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlNodeToRexConverterImpl.java @@ -26,6 +26,7 @@ import org.apache.calcite.sql.SqlLiteral; import org.apache.calcite.sql.SqlTimeLiteral; import org.apache.calcite.sql.SqlTimestampLiteral; +import org.apache.calcite.sql.SqlTimestampTzLiteral; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.util.BitString; import org.apache.calcite.util.DateString; @@ -131,7 +132,7 @@ public class SqlNodeToRexConverterImpl implements SqlNodeToRexConverter { case TIMESTAMP_TZ: return rexBuilder.makeTimestampTzLiteral( literal.getValueAs(TimestampWithTimeZoneString.class), - ((SqlTimestampLiteral) literal).getPrec()); + ((SqlTimestampTzLiteral) literal).getPrec()); case TIME: return rexBuilder.makeTimeLiteral( literal.getValueAs(TimeString.class), 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 14195469ab96..5816b12a6520 100644 --- a/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java +++ b/core/src/main/java/org/apache/calcite/util/TimestampWithTimeZoneString.java @@ -173,7 +173,7 @@ public TimestampWithTimeZoneString withTimeZone(TimeZone timeZone) { } @Override public int compareTo(TimestampWithTimeZoneString o) { - return v.compareTo(o.v); + return this.pt.getCalendar().compareTo(o.pt.getCalendar()); } public TimestampWithTimeZoneString round(int precision) { diff --git a/site/_docs/reference.md b/site/_docs/reference.md index 96eb1a25b5a6..5f12b22149d6 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -1682,6 +1682,7 @@ charSet: timeZone: WITHOUT TIME ZONE + | WITH TIME ZONE | WITH LOCAL TIME ZONE {% endhighlight %}