diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteral.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteral.java index f6a8c25a7daa2d..2be7b85071bc69 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteral.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteral.java @@ -27,9 +27,15 @@ import org.apache.doris.nereids.types.DataType; import org.apache.doris.nereids.types.DateTimeV2Type; import org.apache.doris.nereids.types.TimeStampTzType; +import org.apache.doris.nereids.types.coercion.CharacterType; +import org.apache.doris.nereids.util.DateUtils; import org.apache.doris.qe.ConnectContext; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Locale; import java.util.Objects; /** @@ -168,6 +174,8 @@ protected Expression uncheckedCastTo(DataType targetType) throws AnalysisExcepti if (targetType.isTimeStampTzType()) { return new TimestampTzLiteral((TimeStampTzType) targetType, year, month, day, hour, minute, second, microSecond); + } else if (targetType.isStringLikeType()) { + return uncheckedCastToString((CharacterType) targetType); } else if (targetType.isDateTimeV2Type()) { DateTimeV2Literal dtV2Lit = new DateTimeV2Literal((DateTimeV2Type) targetType, year, month, day, hour, minute, second, microSecond); @@ -180,6 +188,37 @@ protected Expression uncheckedCastTo(DataType targetType) throws AnalysisExcepti throw new AnalysisException(String.format("Cast from %s to %s not supported", this, targetType)); } + @Override + protected String castValueToString() { + ZoneId sessionZone = DateUtils.getTimeZone(); + ZonedDateTime localDateTime = toJavaDateType().atZone(ZoneId.of("UTC")).withZoneSameInstant(sessionZone); + return formatDateTime(localDateTime.toLocalDateTime()) + formatOffset(localDateTime.getOffset()); + } + + private static String formatOffset(ZoneOffset offset) { + // Keep FE constant folding aligned with BE TimestampTzValue::to_string() + // in be/src/core/value/timestamptz_value.cpp. BE renders the numeric offset + // from seconds as sign + HH:MM, instead of using library ids such as "Z". + int totalSeconds = offset.getTotalSeconds(); + int absSeconds = Math.abs(totalSeconds); + int hours = absSeconds / 3600; + int minutes = (absSeconds % 3600) / 60; + return String.format(Locale.ROOT, "%s%02d:%02d", totalSeconds < 0 ? "-" : "+", hours, minutes); + } + + private String formatDateTime(LocalDateTime dateTime) { + String base = String.format(Locale.ROOT, "%04d-%02d-%02d %02d:%02d:%02d", dateTime.getYear(), + dateTime.getMonthValue(), dateTime.getDayOfMonth(), dateTime.getHour(), + dateTime.getMinute(), dateTime.getSecond()); + int scale = getDataType().getScale(); + if (scale <= 0) { + return base; + } + long scaledMicroSecond = (dateTime.getNano() / 1000) + / (int) Math.pow(10, TimeStampTzType.MAX_SCALE - scale); + return base + "." + String.format(Locale.ROOT, "%0" + scale + "d", scaledMicroSecond); + } + public Expression plusDays(long days) { return fromJavaDateType(toJavaDateType().plusDays(days), getDataType().getScale()); } diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java index a1961a61fbb1fe..2bccf146608ea9 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/TimestampTzLiteralTest.java @@ -17,14 +17,26 @@ package org.apache.doris.nereids.trees.expressions.literal; +import org.apache.doris.nereids.rules.expression.rules.FoldConstantRuleOnFE; +import org.apache.doris.nereids.trees.expressions.Cast; import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.types.CharType; +import org.apache.doris.nereids.types.StringType; import org.apache.doris.nereids.types.TimeStampTzType; +import org.apache.doris.nereids.types.VarcharType; +import org.apache.doris.qe.ConnectContext; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class TimestampTzLiteralTest { + @AfterEach + void tearDown() { + ConnectContext.remove(); + } + @Test void testConstructorsAndParsing() { TimestampTzLiteral literal; @@ -300,4 +312,50 @@ void testPlusSecondMicrosecond() { Assertions.assertEquals(58, result.second); Assertions.assertEquals(900000, result.microSecond); } + + @Test + void testCastToStringUsesSessionTimeZone() throws Exception { + setSessionTimeZone("+12:34"); + TimestampTzLiteral literal = new TimestampTzLiteral( + TimeStampTzType.SYSTEM_DEFAULT, 2023, 7, 13, 19, 26, 0, 0); + Assertions.assertEquals("2023-07-13 19:26:00", literal.getStringValue()); + + Expression folded = FoldConstantRuleOnFE.evaluate(new Cast(literal, StringType.INSTANCE), null); + Assertions.assertInstanceOf(StringLiteral.class, folded); + Assertions.assertEquals("2023-07-14 08:00:00+12:34", + ((StringLiteral) folded).getStringValue()); + + Expression varchar = literal.uncheckedCastTo(VarcharType.createVarcharType(64)); + Assertions.assertInstanceOf(VarcharLiteral.class, varchar); + Assertions.assertEquals("2023-07-14 08:00:00+12:34", + ((VarcharLiteral) varchar).getStringValue()); + + Expression character = literal.uncheckedCastTo(CharType.createCharType(64)); + Assertions.assertInstanceOf(CharLiteral.class, character); + Assertions.assertEquals("2023-07-14 08:00:00+12:34", + ((CharLiteral) character).getStringValue()); + + setSessionTimeZone("+00:00"); + Expression utcFolded = FoldConstantRuleOnFE.evaluate(new Cast(literal, StringType.INSTANCE), null); + Assertions.assertInstanceOf(StringLiteral.class, utcFolded); + Assertions.assertEquals("2023-07-13 19:26:00+00:00", + ((StringLiteral) utcFolded).getStringValue()); + } + + @Test + void testCastToStringPreservesScale() { + setSessionTimeZone("+12:34"); + TimestampTzLiteral literal = new TimestampTzLiteral("2023-07-13 22:28:18.456789+05:00"); + + Expression folded = FoldConstantRuleOnFE.evaluate(new Cast(literal, StringType.INSTANCE), null); + Assertions.assertInstanceOf(StringLiteral.class, folded); + Assertions.assertEquals("2023-07-14 06:02:18.456789+12:34", + ((StringLiteral) folded).getStringValue()); + } + + private void setSessionTimeZone(String timeZone) { + ConnectContext context = new ConnectContext(); + context.getSessionVariable().setTimeZone(timeZone); + context.setThreadLocalInfo(); + } }