This is an automated email from the ASF dual-hosted git repository.
gavinchou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f631250a110 [fix](fe) Accept spaced TIMESTAMPTZ offsets in range
partition bounds (#64795)
f631250a110 is described below
commit f631250a1105f6d4f310d50f81db016557a23a4a
Author: starocean999 <[email protected]>
AuthorDate: Fri Jul 3 16:05:48 2026 +0800
[fix](fe) Accept spaced TIMESTAMPTZ offsets in range partition bounds
(#64795)
---
.../org/apache/doris/catalog/PartitionKey.java | 20 +-
.../doris/clone/DynamicPartitionScheduler.java | 45 +++-
.../doris/mtmv/MTMVPartitionExprDateTrunc.java | 21 +-
.../expressions/literal/StringLikeLiteral.java | 12 +-
.../expressions/literal/TimestampTzLiteral.java | 162 ++++++-----
.../doris/nereids/util/TypeCoercionUtils.java | 28 +-
.../doris/analysis/PartitionExprUtilTest.java | 44 +++
.../apache/doris/catalog/CreateTableLikeTest.java | 157 +++++++++++
.../doris/catalog/DynamicPartitionTableTest.java | 299 +++++++++++++++++++++
.../org/apache/doris/catalog/PartitionKeyTest.java | 158 +++++++++++
...TMVRelatedPartitionDescRollUpGeneratorTest.java | 72 +++++
.../expressions/literal/StringLikeLiteralTest.java | 41 +++
.../literal/TimestampTzLiteralTest.java | 122 +++++++++
...est_timestamptz_partition_boundary_timezone.out | 26 ++
..._timestamptz_partition_boundary_timezone.groovy | 257 ++++++++++++++++++
15 files changed, 1353 insertions(+), 111 deletions(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/PartitionKey.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/PartitionKey.java
index ad8819567cb..3832876cd84 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/PartitionKey.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/PartitionKey.java
@@ -31,16 +31,13 @@ import org.apache.doris.analysis.ToSqlParams;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.io.Text;
import org.apache.doris.common.io.Writable;
-import
org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeExtractAndTransform;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.Literal;
-import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.TimeStampTzType;
import org.apache.doris.persist.gson.GsonUtils;
-import org.apache.doris.qe.ConnectContext;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
@@ -147,14 +144,7 @@ public class PartitionKey implements
Comparable<PartitionKey>, Writable {
} else if (type.isDatetimeV2()) {
return new DateTimeV2Literal(value);
} else if (type.isTimeStampTz()) {
- DateTimeV2Literal literal = new DateTimeV2Literal(value);
- DateTimeV2Literal dtV2Lit = (DateTimeV2Literal)
(DateTimeExtractAndTransform.convertTz(
- literal,
- new
StringLiteral(ConnectContext.get().getSessionVariable().timeZone),
- new StringLiteral("UTC")));
- return new TimestampTzLiteral((TimeStampTzType)
DataType.fromCatalogType(type),
- dtV2Lit.getYear(), dtV2Lit.getMonth(),
dtV2Lit.getDay(),
- dtV2Lit.getHour(), dtV2Lit.getMinute(),
dtV2Lit.getSecond(), dtV2Lit.getMicroSecond());
+ return
TimestampTzLiteral.fromSessionTimeZone((TimeStampTzType)
DataType.fromCatalogType(type), value);
}
} catch (Exception e) {
@@ -199,6 +189,14 @@ public class PartitionKey implements
Comparable<PartitionKey>, Writable {
for (int i = 0; i < values.size(); i++) {
if (values.get(i).isNullPartition()) {
partitionKey.keys.add(NullLiteral.create(types.get(i)));
+ } else if (types.get(i).isTimeStampTz()) {
+ // Route TIMESTAMPTZ through the Nereids-aware parser (same as
+ // createPartitionKey) so that named/lowercase timezone values
+ // like "2024-01-15 20:00:00 Asia/Shanghai" are parsed
correctly.
+ // The legacy DateLiteralUtils path only recognizes uppercase
+ // timezone names and a subset of offsets.
+ Literal dateTimeLiteral =
getDateTimeLiteral(values.get(i).getStringValue(), types.get(i));
+ partitionKey.keys.add(dateTimeLiteral.toLegacyLiteral());
} else {
partitionKey.keys.add(values.get(i).getValue(types.get(i)));
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/clone/DynamicPartitionScheduler.java
b/fe/fe-core/src/main/java/org/apache/doris/clone/DynamicPartitionScheduler.java
index 355a88aeb90..4367b27cc39 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/clone/DynamicPartitionScheduler.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/clone/DynamicPartitionScheduler.java
@@ -36,8 +36,10 @@ import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.Partition;
import org.apache.doris.catalog.PartitionItem;
import org.apache.doris.catalog.PartitionKey;
+import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.RangePartitionInfo;
import org.apache.doris.catalog.RangePartitionItem;
+import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.Table;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.Config;
@@ -54,8 +56,10 @@ import org.apache.doris.common.util.RangeUtils;
import org.apache.doris.common.util.TimeUtils;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.meta.MetaContext;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
import org.apache.doris.nereids.trees.plans.commands.info.AddPartitionOp;
import org.apache.doris.nereids.trees.plans.commands.info.DropPartitionOp;
+import org.apache.doris.nereids.types.TimeStampTzType;
import org.apache.doris.nereids.util.DateUtils;
import org.apache.doris.persist.PartitionPersistInfo;
import org.apache.doris.thrift.TStorageMedium;
@@ -81,6 +85,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TimeZone;
import java.util.stream.Collectors;
/**
@@ -302,6 +307,12 @@ public class DynamicPartitionScheduler extends
MasterDaemon {
dynamicPartitionProperty, now, idx, partitionFormat);
String nextBorder = DynamicPartitionUtil.getPartitionRangeString(
dynamicPartitionProperty, now, idx + 1, partitionFormat);
+ // Save original border for partition name (must not contain UTC
suffix)
+ String prevBorderForName = prevBorder;
+ prevBorder = convertToUtcTimestamp(partitionColumn, prevBorder,
+ dynamicPartitionProperty.getTimeZone());
+ nextBorder = convertToUtcTimestamp(partitionColumn, nextBorder,
+ dynamicPartitionProperty.getTimeZone());
PartitionValue lowerValue = new PartitionValue(prevBorder);
PartitionValue upperValue = new PartitionValue(nextBorder);
@@ -370,7 +381,7 @@ public class DynamicPartitionScheduler extends MasterDaemon
{
String partitionName = dynamicPartitionProperty.getPrefix()
+
DynamicPartitionUtil.getFormattedPartitionName(dynamicPartitionProperty.getTimeZone(),
- prevBorder, dynamicPartitionProperty.getTimeUnit());
+ prevBorderForName, dynamicPartitionProperty.getTimeUnit());
SinglePartitionDesc rangePartitionDesc = new
SinglePartitionDesc(true, partitionName,
partitionKeyDesc, partitionProperties);
@@ -483,9 +494,29 @@ public class DynamicPartitionScheduler extends
MasterDaemon {
partitionProperties.put(PropertyAnalyzer.PROPERTIES_DATA_BASE_TIME,
baseTime);
}
+ /**
+ * For TIMESTAMPTZ partition columns, convert the border string from the
partition's
+ * timezone to a UTC timestamp string (with +00:00 suffix). This ensures
partition
+ * boundaries are stored as UTC timestamps, so the partition range aligns
to
+ * 00:00:00—24:00:00 in UTC regardless of the configured partition
timezone.
+ */
+ private static String convertToUtcTimestamp(Column column, String border,
TimeZone timeZone) {
+ if (column.getDataType() == PrimitiveType.TIMESTAMPTZ) {
+ TimestampTzLiteral utcLiteral = TimestampTzLiteral.fromTimeZone(
+ TimeStampTzType.of(((ScalarType)
column.getType()).getScalarScale()),
+ border, timeZone.toZoneId().toString());
+ return utcLiteral.getStringValue();
+ }
+ return border;
+ }
+
private Range<PartitionKey> getClosedRange(Database db, OlapTable
olapTable, Column partitionColumn,
String partitionFormat, String lowerBorderOfReservedHistory,
String upperBorderOfReservedHistory) {
Range<PartitionKey> reservedHistoryPartitionKeyRange = null;
+ lowerBorderOfReservedHistory = convertToUtcTimestamp(partitionColumn,
lowerBorderOfReservedHistory,
+
olapTable.getTableProperty().getDynamicPartitionProperty().getTimeZone());
+ upperBorderOfReservedHistory = convertToUtcTimestamp(partitionColumn,
upperBorderOfReservedHistory,
+
olapTable.getTableProperty().getDynamicPartitionProperty().getTimeZone());
PartitionValue lowerBorderPartitionValue = new
PartitionValue(lowerBorderOfReservedHistory);
PartitionValue upperBorderPartitionValue = new
PartitionValue(upperBorderOfReservedHistory);
try {
@@ -526,6 +557,10 @@ public class DynamicPartitionScheduler extends
MasterDaemon {
now, realStart, partitionFormat);
String limitBorder =
DynamicPartitionUtil.getPartitionRangeString(dynamicPartitionProperty,
now, 0, partitionFormat);
+ lowerBorder = convertToUtcTimestamp(partitionColumn, lowerBorder,
+ dynamicPartitionProperty.getTimeZone());
+ limitBorder = convertToUtcTimestamp(partitionColumn, limitBorder,
+ dynamicPartitionProperty.getTimeZone());
PartitionValue lowerPartitionValue = new PartitionValue(lowerBorder);
PartitionValue limitPartitionValue = new PartitionValue(limitBorder);
List<Range<PartitionKey>> reservedHistoryPartitionKeyRangeList = new
ArrayList<Range<PartitionKey>>();
@@ -625,6 +660,14 @@ public class DynamicPartitionScheduler extends
MasterDaemon {
String partitionFormat =
DynamicPartitionUtil.getPartitionFormat(partitionColumn);
String currentTimeStr =
DateTimeFormatter.ofPattern(partitionFormat).format(now);
+ // For TIMESTAMPTZ columns, normalize the cutoff time to UTC so it
+ // matches the stored partition bounds, which are also stored as UTC.
+ // Without this, a suffix-free currentTimeStr formatted in the JVM
+ // timezone would be parsed as UTC via
TimestampTzLiteral.fromSessionTimeZone(),
+ // shifting the cutoff and potentially dropping the current UTC-day
partition.
+ currentTimeStr = convertToUtcTimestamp(partitionColumn, currentTimeStr,
+ TimeZone.getTimeZone(DateUtils.getTimeZone()));
+
PartitionValue currentTimeValue = new PartitionValue(currentTimeStr);
PartitionKey currentTimeKey;
try {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPartitionExprDateTrunc.java
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPartitionExprDateTrunc.java
index 79e95fa56d9..6a23cabdb93 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPartitionExprDateTrunc.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/mtmv/MTMVPartitionExprDateTrunc.java
@@ -34,7 +34,9 @@ import
org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeE
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
import org.apache.doris.nereids.trees.expressions.literal.DateV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
+import
org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
@@ -179,6 +181,14 @@ public class MTMVPartitionExprDateTrunc implements
MTMVPartitionExprService {
private DateTimeV2Literal strToDate(String value,
Optional<String> dateFormat) throws AnalysisException {
try {
+ if (DateTimeChecker.hasTimeZone(value)) {
+ // For TIMESTAMPTZ values, parse preserving UTC semantics.
+ // DateTimeV2Literal would convert to session timezone, which
would
+ // produce incorrect MV partition boundaries when session tz
!= UTC.
+ TimestampTzLiteral tzLiteral = new TimestampTzLiteral(value);
+ return new DateTimeV2Literal(tzLiteral.getYear(),
tzLiteral.getMonth(), tzLiteral.getDay(),
+ tzLiteral.getHour(), tzLiteral.getMinute(),
tzLiteral.getSecond());
+ }
return new DateTimeV2Literal(value);
} catch (Exception e) {
if (!dateFormat.isPresent()) {
@@ -237,11 +247,18 @@ public class MTMVPartitionExprDateTrunc implements
MTMVPartitionExprService {
if (partitionColumnType.isDate() || partitionColumnType.isDateV2()) {
return String.format(PartitionExprUtil.DATE_FORMATTER,
literal.getYear(), literal.getMonth(),
literal.getDay());
- } else if (partitionColumnType.isDatetime() ||
partitionColumnType.isDatetimeV2()
- || partitionColumnType.isTimeStampTz()) {
+ } else if (partitionColumnType.isDatetime() ||
partitionColumnType.isDatetimeV2()) {
return String.format(PartitionExprUtil.DATETIME_FORMATTER,
literal.getYear(), literal.getMonth(), literal.getDay(),
literal.getHour(), literal.getMinute(),
literal.getSecond());
+ } else if (partitionColumnType.isTimeStampTz()) {
+ // The internal DateTimeV2Literal values are always in UTC after
truncation.
+ // Emit an explicit +00:00 suffix so that downstream consumers
+ // (PartitionKey.createPartitionKey ->
TimestampTzLiteral.fromSessionTimeZone)
+ // interpret the value as UTC rather than session-local time.
+ return String.format(PartitionExprUtil.DATETIME_FORMATTER,
+ literal.getYear(), literal.getMonth(), literal.getDay(),
+ literal.getHour(), literal.getMinute(),
literal.getSecond()) + "+00:00";
} else {
throw new AnalysisException(
"MTMV not support partition with column type : " +
partitionColumnType);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteral.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteral.java
index 621b8d6fa2b..148c3efb852 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteral.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteral.java
@@ -21,14 +21,12 @@ import org.apache.doris.analysis.LiteralExpr;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.exceptions.CastException;
import org.apache.doris.nereids.trees.expressions.Expression;
-import
org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeExtractAndTransform;
import
org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
import org.apache.doris.nereids.types.DataType;
import org.apache.doris.nereids.types.DateTimeType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.TimeStampTzType;
import org.apache.doris.nereids.types.TimeV2Type;
-import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.SessionVariable;
import com.google.common.base.Preconditions;
@@ -148,8 +146,7 @@ public abstract class StringLikeLiteral extends Literal
implements ComparableLit
return new DateTimeLiteral((DateTimeType) targetType,
datetime.year, datetime.month, datetime.day,
datetime.hour, datetime.minute, datetime.second,
datetime.microSecond);
} else if (targetType.isTimeStampTzType()) {
- // Explicit offsets must not round-trip through session local
time; that loses the selected
- // branch in DST fold hours. Wildcard targets still need a
concrete scale before parsing.
+ // Wildcard targets still need a concrete scale before parsing.
TimeStampTzType timeStampTzType = (TimeStampTzType) targetType;
if (timeStampTzType.getScale() < 0) {
timeStampTzType = TimeStampTzType.forTypeFromString(value);
@@ -157,11 +154,8 @@ public abstract class StringLikeLiteral extends Literal
implements ComparableLit
if (DateTimeChecker.hasTimeZone(value)) {
return new TimestampTzLiteral(timeStampTzType, value);
}
- DateTimeV2Literal expression = castToDateTime(DateTimeV2Type.MAX,
strictCast, true);
- expression = (DateTimeV2Literal)
(DateTimeExtractAndTransform.convertTz(expression,
- new
StringLiteral(ConnectContext.get().getSessionVariable().timeZone), new
StringLiteral("UTC")));
- return new TimestampTzLiteral(timeStampTzType, expression.year,
expression.month,
- expression.day, expression.hour, expression.minute,
expression.second, expression.microSecond);
+ DateTimeV2Literal datetime = castToDateTime(DateTimeV2Type.MAX,
strictCast, true);
+ return TimestampTzLiteral.fromSessionTimeZone(timeStampTzType,
datetime);
} else if (targetType.isDateTimeV2Type()) {
return castToDateTime(targetType, strictCast, true);
} else if (targetType.isFloatType()) {
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 f6a8c25a7da..7caed5ecc01 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
@@ -23,12 +23,17 @@ import
org.apache.doris.nereids.exceptions.NotSupportedException;
import org.apache.doris.nereids.exceptions.UnboundException;
import org.apache.doris.nereids.trees.expressions.Expression;
import
org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeExtractAndTransform;
+import
org.apache.doris.nereids.trees.expressions.literal.format.DateTimeChecker;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
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.qe.ConnectContext;
+import com.google.common.base.Preconditions;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
import java.time.LocalDateTime;
import java.util.Objects;
@@ -40,6 +45,8 @@ public class TimestampTzLiteral extends DateTimeLiteral {
public static final TimestampTzLiteral USE_IN_FLOOR_CEIL
= new TimestampTzLiteral(0001L, 01L, 01L, 0L, 0L, 0L, 0L);
+ private static final Logger LOG =
LogManager.getLogger(TimestampTzLiteral.class);
+
public TimestampTzLiteral(String s) {
this(TimeStampTzType.forTypeFromString(s), s);
}
@@ -64,6 +71,66 @@ public class TimestampTzLiteral extends DateTimeLiteral {
roundMicroSecond(dateType.getScale());
}
+ /**
+ * Build a TIMESTAMPTZ literal from a session-local datetime string.
+ * Strings with an explicit zone/offset keep their own timezone semantics;
+ * strings without one are interpreted in the current session timezone and
converted to UTC.
+ */
+ public static TimestampTzLiteral fromSessionTimeZone(TimeStampTzType
dateType, String s) {
+ if (DateTimeChecker.hasTimeZone(s)) {
+ return new TimestampTzLiteral(dateType, s);
+ }
+
+ return fromSessionTimeZone(dateType, new DateTimeV2Literal(s));
+ }
+
+ /**
+ * fromSessionTimeZone
+ */
+ public static TimestampTzLiteral fromSessionTimeZone(TimeStampTzType
dateType, DateTimeV2Literal literal) {
+ return fromTimeZone(dateType, literal, getSessionTimeZone());
+ }
+
+ /**
+ * Build a TIMESTAMPTZ literal from a datetime string, explicitly
specifying the source timezone.
+ * Strings with an explicit zone/offset keep their own timezone semantics;
+ * strings without one are converted from the given timeZone to UTC.
+ * @param timeZone Fallback timezone used only when the string has no
explicit offset
+ */
+ public static TimestampTzLiteral fromTimeZone(TimeStampTzType dateType,
String s, String timeZone) {
+ if (DateTimeChecker.hasTimeZone(s)) {
+ return new TimestampTzLiteral(dateType, s);
+ }
+ return fromTimeZone(dateType, new DateTimeV2Literal(s), timeZone);
+ }
+
+ /**
+ * Build a TIMESTAMPTZ literal from a DateTimeV2Literal, explicitly
specifying the source timezone.
+ */
+ public static TimestampTzLiteral fromTimeZone(TimeStampTzType dateType,
DateTimeV2Literal literal,
+ String timeZone) {
+ DateTimeV2Literal utcLiteral = (DateTimeV2Literal)
DateTimeExtractAndTransform.convertTz(
+ literal,
+ new StringLiteral(timeZone),
+ new StringLiteral("UTC"));
+ return new TimestampTzLiteral(dateType,
+ utcLiteral.year,
+ utcLiteral.month,
+ utcLiteral.day,
+ utcLiteral.hour,
+ utcLiteral.minute,
+ utcLiteral.second,
+ utcLiteral.microSecond);
+ }
+
+ private static String getSessionTimeZone() {
+ ConnectContext context = ConnectContext.get();
+ if (context == null && LOG.isDebugEnabled()) {
+ LOG.debug("context is null, session time zone is fallback to UTC");
+ }
+ return context == null ? "UTC" : context.getSessionVariable().timeZone;
+ }
+
@Override
public TimeStampTzType getDataType() throws UnboundException {
return (TimeStampTzType) super.getDataType();
@@ -92,72 +159,7 @@ public class TimestampTzLiteral extends DateTimeLiteral {
@Override
public String getStringValue() {
- int scale = getDataType().getScale();
- if (scale <= 0) {
- return super.getStringValue();
- }
-
- if (0 <= year && year <= 9999 && 0 <= month && month <= 99 && 0 <= day
&& day <= 99
- && 0 <= hour && hour <= 99 && 0 <= minute && minute <= 99 && 0
<= second && second <= 99
- && 0 <= microSecond && microSecond <= MAX_MICROSECOND) {
- char[] format = new char[] {
- '0', '0', '0', '0', '-', '0', '0', '-', '0', '0', ' ',
'0', '0', ':', '0', '0', ':', '0', '0',
- '.', '0', '0', '0', '0', '0', '0'};
- int offset = 3;
- long year = this.year;
- while (year > 0) {
- format[offset--] = (char) ('0' + (year % 10));
- year /= 10;
- }
-
- offset = 6;
- long month = this.month;
- while (month > 0) {
- format[offset--] = (char) ('0' + (month % 10));
- month /= 10;
- }
-
- offset = 9;
- long day = this.day;
- while (day > 0) {
- format[offset--] = (char) ('0' + (day % 10));
- day /= 10;
- }
-
- offset = 12;
- long hour = this.hour;
- while (hour > 0) {
- format[offset--] = (char) ('0' + (hour % 10));
- hour /= 10;
- }
-
- offset = 15;
- long minute = this.minute;
- while (minute > 0) {
- format[offset--] = (char) ('0' + (minute % 10));
- minute /= 10;
- }
-
- offset = 18;
- long second = this.second;
- while (second > 0) {
- format[offset--] = (char) ('0' + (second % 10));
- second /= 10;
- }
-
- offset = 19 + scale;
- long microSecond = (int) (this.microSecond / Math.pow(10,
TimeStampTzType.MAX_SCALE - scale));
- while (microSecond > 0) {
- format[offset--] = (char) ('0' + (microSecond % 10));
- microSecond /= 10;
- }
- return String.valueOf(format, 0, 20 + scale);
- }
-
- return String.format("%04d-%02d-%02d %02d:%02d:%02d"
- + (scale > 0 ? ".%0" + scale + "d" : ""),
- year, month, day, hour, minute, second,
- (int) (microSecond / Math.pow(10, TimeStampTzType.MAX_SCALE -
scale)));
+ return toLegacyLiteral().getStringValue();
}
@Override
@@ -436,6 +438,34 @@ public class TimestampTzLiteral extends DateTimeLiteral {
microSecond / (int) Math.pow(10, 6 - newScale) * (int)
Math.pow(10, 6 - newScale));
}
+ @Override
+ protected void roundMicroSecond(int scale) {
+ Preconditions.checkArgument(scale >= 0 && scale <=
DateTimeV2Type.MAX_SCALE,
+ "invalid datetime v2 scale: %s", scale);
+ double factor = Math.pow(10, 6 - scale);
+
+ this.microSecond = Math.round(this.microSecond / factor) * (int)
factor;
+
+ if (this.microSecond >= 1000000) {
+ // Use internal fields directly instead of parsing
getStringValue(),
+ // because getStringValue() includes a timezone suffix (+00:00)
that
+ // the parent class's DATE_TIME_FORMATTER_TO_MICRO_SECOND cannot
parse.
+ LocalDateTime localDateTime = LocalDateTime.of(
+ (int) year, (int) month, (int) day,
+ (int) hour, (int) minute, (int) second).plusSeconds(1);
+ this.year = localDateTime.getYear();
+ this.month = localDateTime.getMonthValue();
+ this.day = localDateTime.getDayOfMonth();
+ this.hour = localDateTime.getHour();
+ this.minute = localDateTime.getMinute();
+ this.second = localDateTime.getSecond();
+ this.microSecond -= 1000000;
+ }
+ if (checkRange() || checkDate(year, month, day)) {
+ throw new AnalysisException("datetime literal [" + toString() + "]
is out of range");
+ }
+ }
+
public static Expression fromJavaDateType(LocalDateTime dateTime) {
return fromJavaDateType(dateTime, 6);
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
index 84f72f954c0..c19250e4060 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java
@@ -43,7 +43,6 @@ import org.apache.doris.nereids.trees.expressions.Multiply;
import org.apache.doris.nereids.trees.expressions.SubqueryExpr;
import org.apache.doris.nereids.trees.expressions.Subtract;
import org.apache.doris.nereids.trees.expressions.functions.BoundFunction;
-import
org.apache.doris.nereids.trees.expressions.functions.executable.DateTimeExtractAndTransform;
import org.apache.doris.nereids.trees.expressions.functions.scalar.CreateMap;
import org.apache.doris.nereids.trees.expressions.literal.BigIntLiteral;
import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
@@ -110,7 +109,6 @@ import
org.apache.doris.nereids.types.coercion.FractionalType;
import org.apache.doris.nereids.types.coercion.IntegralType;
import org.apache.doris.nereids.types.coercion.NumericType;
import org.apache.doris.nereids.types.coercion.PrimitiveType;
-import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.GlobalVariable;
import org.apache.doris.qe.SessionVariable;
@@ -655,27 +653,13 @@ public class TypeCoercionUtils {
&& DateTimeChecker.isValidDateTime(value)) {
ret = DateTimeLiteral.parseDateTimeLiteral(value,
true).orElse(null);
} else if (dataType.isTimeStampTzType() &&
DateTimeChecker.isValidDateTime(value)) {
- if (DateTimeChecker.hasTimeZone(value)) {
- // Signature search can pass TIMESTAMPTZ(*) here.
TimestampTzLiteral rounds by scale,
- // so derive a concrete scale from the literal before
preserving its explicit offset.
- TimeStampTzType timeStampTzType = (TimeStampTzType)
dataType;
- if (timeStampTzType.getScale() < 0) {
- timeStampTzType =
TimeStampTzType.forTypeFromString(value);
- }
- ret = new TimestampTzLiteral(timeStampTzType, value);
- } else {
- DateTimeV2Literal dtV2Lit = (DateTimeV2Literal)
DateTimeLiteral
- .parseDateTimeLiteral(value,
true).orElse(null);
- if (dtV2Lit != null) {
- dtV2Lit = (DateTimeV2Literal)
(DateTimeExtractAndTransform.convertTz(
- dtV2Lit,
- new
StringLiteral(ConnectContext.get().getSessionVariable().timeZone),
- new StringLiteral("UTC")));
- ret = new TimestampTzLiteral(dtV2Lit.getYear(),
dtV2Lit.getMonth(), dtV2Lit.getDay(),
- dtV2Lit.getHour(), dtV2Lit.getMinute(),
dtV2Lit.getSecond(),
- dtV2Lit.getMicroSecond());
- }
+ // Signature search can pass TIMESTAMPTZ(*) here.
TimestampTzLiteral rounds by scale,
+ // so derive a concrete scale from the literal before parsing.
+ TimeStampTzType timeStampTzType = (TimeStampTzType) dataType;
+ if (timeStampTzType.getScale() < 0 ||
timeStampTzType.getScale() == TimeStampTzType.MAX_SCALE) {
+ timeStampTzType = TimeStampTzType.forTypeFromString(value);
}
+ ret = TimestampTzLiteral.fromSessionTimeZone(timeStampTzType,
value);
} else if ((dataType.isDateV2Type() || dataType.isDateType()) &&
DateTimeChecker.isValidDateTime(value)) {
Result<DateLiteral, AnalysisException> parseResult =
DateV2Literal.parseDateLiteral(value, true);
if (parseResult.isOk()) {
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/analysis/PartitionExprUtilTest.java
b/fe/fe-core/src/test/java/org/apache/doris/analysis/PartitionExprUtilTest.java
index 6bfe689f421..2166d69a8c3 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/analysis/PartitionExprUtilTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/analysis/PartitionExprUtilTest.java
@@ -224,4 +224,48 @@ public class PartitionExprUtilTest extends
TestWithFeService {
Assertions.assertNotNull(p2);
Assertions.assertEquals(p1.getDistributionInfo().getBucketNum(),
p2.getDistributionInfo().getBucketNum());
}
+
+ @Test
+ public void testAutoPartitionDateTruncTimestampTzKeepsUtcBoundary() throws
Exception {
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("+00:00");
+ String createOlapTblStmt = "CREATE TABLE
test.auto_timestamptz_day_partition (\n"
+ + " id INT,\n"
+ + " ts_tz TIMESTAMPTZ(6) NOT NULL\n"
+ + ")\n"
+ + "DUPLICATE KEY(id)\n"
+ + "AUTO PARTITION BY RANGE (date_trunc(ts_tz, 'day')) ()\n"
+ + "DISTRIBUTED BY HASH(id) BUCKETS 1\n"
+ + "PROPERTIES(\"replication_num\"=\"1\");";
+
+ createTable(createOlapTblStmt);
+ Database db =
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+ OlapTable table = (OlapTable)
db.getTableOrAnalysisException("auto_timestamptz_day_partition");
+
+ List<List<TNullableStringLiteral>> partitionValues = new
ArrayList<>();
+ List<TNullableStringLiteral> values = new ArrayList<>();
+ TNullableStringLiteral truncatedValue = new
TNullableStringLiteral();
+ truncatedValue.setValue("2024-06-15 00:00:00");
+ values.add(truncatedValue);
+ partitionValues.add(values);
+
+ FrontendServiceImpl impl = new FrontendServiceImpl(exeEnv);
+ TCreatePartitionRequest request = new TCreatePartitionRequest();
+ request.setDbId(db.getId());
+ request.setTableId(table.getId());
+ request.setPartitionValues(partitionValues);
+
+ TCreatePartitionResult result = impl.createPartition(request);
+ Assertions.assertEquals(TStatusCode.OK,
result.getStatus().getStatusCode());
+ Assertions.assertNotNull(table.getPartition("p20240615000000"));
+
+ List<String> createTableStmt = new ArrayList<>();
+ Env.getDdlStmt(table, createTableStmt, null, null, false, true,
-1L);
+ Assertions.assertTrue(createTableStmt.get(0).contains(
+ "PARTITION p20240615000000 VALUES [('2024-06-15
00:00:00.000000+00:00'), ('2024-06-16 00:00:00.000000+00:00'))"));
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableLikeTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableLikeTest.java
index 732c81e5b6b..4368ff70c44 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableLikeTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateTableLikeTest.java
@@ -95,6 +95,12 @@ public class CreateTableLikeTest {
command.run(connectContext, new StmtExecutor(connectContext, sql));
}
+ private static String getCreateTableStmt(Table table) {
+ List<String> createTableStmt = Lists.newArrayList();
+ Env.getDdlStmt(table, createTableStmt, null, null, false, true /* hide
password */, -1L);
+ return createTableStmt.get(0);
+ }
+
private static void checkTableEqual(Table newTable, Table existedTable,
int rollupSize) {
List<String> newCreateTableStmt = Lists.newArrayList();
List<String> newAddRollupStmt = Lists.newArrayList();
@@ -384,6 +390,157 @@ public class CreateTableLikeTest {
existedDbName3, newTblName3, existedTblName3, 1));
}
+ @Test
+ public void
testTimestampTzLessThanPartitionCreateTableAcceptsExplicitOffset() throws
Exception {
+ String tableName = "test_timestamptz_less_than_create";
+ String createTableSql = "CREATE TABLE test." + tableName + " (\n"
+ + " `ts` TIMESTAMPTZ(6) NOT NULL,\n"
+ + " `seq` INT NOT NULL\n"
+ + ")\n"
+ + "UNIQUE KEY(`ts`)\n"
+ + "PARTITION BY RANGE(`ts`) (\n"
+ + " PARTITION p0 VALUES LESS THAN ('2024-01-15 13:00:00
+00:00'),\n"
+ + " PARTITION p1 VALUES LESS THAN ('2024-01-15 14:00:00
+00:00')\n"
+ + ")\n"
+ + "DISTRIBUTED BY HASH(`ts`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\"\n"
+ + ");";
+
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("+00:00");
+ createTable(createTableSql);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("test");
+ OlapTable table = (OlapTable) db.getTableOrDdlException(tableName);
+
+ String createStmt = getCreateTableStmt(table);
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p0 VALUES [('0000-01-01
00:00:00.000000+00:00'), ('2024-01-15 13:00:00.000000+00:00'))"));
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p1 VALUES [('2024-01-15
13:00:00.000000+00:00'), ('2024-01-15 14:00:00.000000+00:00'))"));
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void
testTimestampTzRangePartitionCreateTableAcceptsNamedAndLowercaseZones() throws
Exception {
+ String tableName = "test_timestamptz_named_lowercase_zone_create";
+ String createTableSql = "CREATE TABLE test." + tableName + " (\n"
+ + " `ts` TIMESTAMPTZ(6) NOT NULL,\n"
+ + " `seq` INT NOT NULL\n"
+ + ")\n"
+ + "UNIQUE KEY(`ts`)\n"
+ + "PARTITION BY RANGE(`ts`) (\n"
+ + " PARTITION p1 VALUES [('2024-01-15
20:00:00Asia/Shanghai'), ('2024-01-15 13:00:00 uTc')),\n"
+ + " PARTITION p2 VALUES [('2024-01-15 13:00:00 uTc'),
('2024-01-15 22:00:00 Asia/Shanghai'))\n"
+ + ")\n"
+ + "DISTRIBUTED BY HASH(`ts`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\"\n"
+ + ");";
+
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+
connectContext.getSessionVariable().setTimeZone("America/New_York");
+ createTable(createTableSql);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("test");
+ OlapTable table = (OlapTable) db.getTableOrDdlException(tableName);
+
+ String createStmt = getCreateTableStmt(table);
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p1 VALUES [('2024-01-15
12:00:00.000000+00:00'), ('2024-01-15 13:00:00.000000+00:00'))"));
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p2 VALUES [('2024-01-15
13:00:00.000000+00:00'), ('2024-01-15 14:00:00.000000+00:00'))"));
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void
testTimestampTzLessThanPartitionCreateTableUsesSessionTimezoneWithoutOffset()
throws Exception {
+ String tableName = "test_timestamptz_less_than_session_tz";
+ String createTableSql = "CREATE TABLE test." + tableName + " (\n"
+ + " `ts` TIMESTAMPTZ(6) NOT NULL,\n"
+ + " `seq` INT NOT NULL\n"
+ + ")\n"
+ + "UNIQUE KEY(`ts`)\n"
+ + "PARTITION BY RANGE(`ts`) (\n"
+ + " PARTITION p0 VALUES LESS THAN ('2024-01-15 13:00:00'),\n"
+ + " PARTITION p1 VALUES LESS THAN ('2024-01-15 14:00:00')\n"
+ + ")\n"
+ + "DISTRIBUTED BY HASH(`ts`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\"\n"
+ + ");";
+
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+
connectContext.getSessionVariable().setTimeZone("America/New_York");
+ createTable(createTableSql);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("test");
+ OlapTable table = (OlapTable) db.getTableOrDdlException(tableName);
+
+ String createStmt = getCreateTableStmt(table);
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p0 VALUES [('0000-01-01
00:00:00.000000+00:00'), ('2024-01-15 18:00:00.000000+00:00'))"));
+ Assert.assertTrue(createStmt, createStmt.contains(
+ "PARTITION p1 VALUES [('2024-01-15
18:00:00.000000+00:00'), ('2024-01-15 19:00:00.000000+00:00'))"));
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void testTimestampTzRangePartitionCreateLikeKeepsUtcBoundary()
throws Exception {
+ String sourceTableName = "test_timestamptz_range_like_src";
+ String cloneTableName = "test_timestamptz_range_like_clone";
+ String createSourceTableSql = "CREATE TABLE test." + sourceTableName +
" (\n"
+ + " `ts` TIMESTAMPTZ(6) NOT NULL,\n"
+ + " `seq` INT NOT NULL\n"
+ + ")\n"
+ + "UNIQUE KEY(`ts`)\n"
+ + "PARTITION BY RANGE(`ts`) (\n"
+ + " PARTITION p1 VALUES [('2024-01-15 12:00:00 +00:00'),
('2024-01-15 13:00:00 +00:00')),\n"
+ + " PARTITION p2 VALUES [('2024-01-15 13:00:00 +00:00'),
('2024-01-15 14:00:00 +00:00'))\n"
+ + ")\n"
+ + "DISTRIBUTED BY HASH(`ts`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\",\n"
+ + " \"enable_unique_key_merge_on_write\" = \"true\"\n"
+ + ");";
+ String createLikeSql = "CREATE TABLE test." + cloneTableName + " LIKE
test." + sourceTableName;
+
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("+00:00");
+ createTable(createSourceTableSql);
+
+ connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+ createTableLike(createLikeSql);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrDdlException("test");
+ OlapTable sourceTable = (OlapTable)
db.getTableOrDdlException(sourceTableName);
+ OlapTable cloneTable = (OlapTable)
db.getTableOrDdlException(cloneTableName);
+
+ String sourceCreateStmt = getCreateTableStmt(sourceTable);
+ Assert.assertTrue(sourceCreateStmt.contains(
+ "PARTITION p1 VALUES [('2024-01-15
12:00:00.000000+00:00'), ('2024-01-15 13:00:00.000000+00:00'))"));
+ Assert.assertTrue(sourceCreateStmt.contains(
+ "PARTITION p2 VALUES [('2024-01-15
13:00:00.000000+00:00'), ('2024-01-15 14:00:00.000000+00:00'))"));
+ checkTableEqual(cloneTable, sourceTable, 0);
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
@Test
public void checkSyncedTableWithRollup() throws Exception {
String createTableWithRollup = "CREATE TABLE IF NOT EXISTS
test.table_with_rollup_synced\n" + "(\n"
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/DynamicPartitionTableTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/DynamicPartitionTableTest.java
index 8afaf7d325e..af91938da37 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/catalog/DynamicPartitionTableTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/catalog/DynamicPartitionTableTest.java
@@ -17,6 +17,7 @@
package org.apache.doris.catalog;
+import org.apache.doris.analysis.LiteralExpr;
import org.apache.doris.catalog.MaterializedIndex.IndexExtState;
import org.apache.doris.clone.DynamicPartitionScheduler;
import org.apache.doris.clone.RebalancerTestUtil;
@@ -39,6 +40,7 @@ import org.apache.doris.thrift.TStorageMedium;
import org.apache.doris.utframe.UtFrameUtils;
import com.google.common.collect.Lists;
+import com.google.common.collect.Range;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -49,6 +51,8 @@ import org.junit.rules.ExpectedException;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Collection;
@@ -59,6 +63,7 @@ import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.TimeZone;
import java.util.TreeMap;
import java.util.UUID;
@@ -2014,4 +2019,298 @@ public class DynamicPartitionTableTest {
// due to partition size eq 0, use previous partition's(54th) bucket
num
Assert.assertEquals(53, partitions.get(partitions.size() -
1).getDistributionInfo().getBucketNum());
}
+
+ @Test
+ public void testTimeStampTzDynamicPartition() throws Exception {
+ // Set session timezone to something different from the partition
timezone
+ // to verify scheduler uses dynamic_partition.time_zone, not session
timezone.
+ // With time_zone = "+00:00" and session = "Asia/Shanghai" (UTC+8),
+ // a session-timezone leak would shift bounds by 8 hours (hour=16, not
00).
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+
+ String createOlapTblStmt = "CREATE TABLE
test.`timestamptz_dynamic_partition` (\n"
+ + " `k1` TIMESTAMPTZ NULL COMMENT \"\",\n"
+ + " `k2` int NULL COMMENT \"\"\n"
+ + ") ENGINE=OLAP\n"
+ + "DUPLICATE KEY(`k1`, `k2`)\n"
+ + "PARTITION BY RANGE(`k1`)\n"
+ + "()\n"
+ + "DISTRIBUTED BY HASH(`k2`) BUCKETS 3\n"
+ + "PROPERTIES (\n"
+ + "\"replication_num\" = \"1\",\n"
+ + "\"dynamic_partition.enable\" = \"true\",\n"
+ + "\"dynamic_partition.start\" = \"-3\",\n"
+ + "\"dynamic_partition.end\" = \"3\",\n"
+ + "\"dynamic_partition.create_history_partition\" =
\"true\",\n"
+ + "\"dynamic_partition.time_unit\" = \"day\",\n"
+ + "\"dynamic_partition.prefix\" = \"p\",\n"
+ + "\"dynamic_partition.buckets\" = \"1\",\n"
+ + "\"dynamic_partition.time_zone\" = \"+00:00\"\n"
+ + ");";
+ createTable(createOlapTblStmt);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+ OlapTable table = (OlapTable)
db.getTableOrAnalysisException("timestamptz_dynamic_partition");
+ Assert.assertTrue(table.dynamicPartitionExists());
+
+ // Execute dynamic partition scheduling
+ Env.getCurrentEnv().getDynamicPartitionScheduler()
+ .executeDynamicPartitionFirstTime(db.getId(),
table.getId());
+
+ // Verify total partitions (7 = start(-3) to end(3), inclusive)
+ int partitionCount = table.getPartitionNames().size();
+ Assert.assertEquals(7, partitionCount);
+
+ // Verify partition names are clean and partition values are UTC
timestamps
+ RangePartitionInfo partitionInfo = (RangePartitionInfo)
table.getPartitionInfo();
+ for (Map.Entry<Long, PartitionItem> entry :
partitionInfo.getIdToItem(false).entrySet()) {
+ RangePartitionItem item = (RangePartitionItem)
entry.getValue();
+
+ // Verify the partition name is clean
+ String partitionName =
table.getPartition(entry.getKey()).getName();
+ Assert.assertTrue("Partition name should start with 'p': " +
partitionName,
+ partitionName.startsWith("p"));
+ Assert.assertEquals("Partition name should be exactly 9 chars
(p + yyyyMMdd): " + partitionName,
+ 9, partitionName.length());
+
+ // Verify the range endpoints are valid and correctly ordered
+ Range<PartitionKey> range = item.getItems();
+ PartitionKey lower = range.lowerEndpoint();
+ PartitionKey upper = range.upperEndpoint();
+ Assert.assertTrue("lower must be < upper: " + range,
+ lower.compareTo(upper) < 0);
+
+ // Verify partition keys are UTC timestamps (with +00:00
suffix)
+ List<LiteralExpr> lowerKeys = lower.getKeys();
+ Assert.assertEquals(1, lowerKeys.size());
+ String lowerStr = lowerKeys.get(0).getStringValue();
+ Assert.assertTrue("Lower key must be UTC with +00:00 suffix: "
+ lowerStr,
+ lowerStr.contains("+00:00"));
+
+ List<LiteralExpr> upperKeys = upper.getKeys();
+ Assert.assertEquals(1, upperKeys.size());
+ String upperStr = upperKeys.get(0).getStringValue();
+ Assert.assertTrue("Upper key must be UTC with +00:00 suffix: "
+ upperStr,
+ upperStr.contains("+00:00"));
+
+ // With time_zone = "+00:00", partition boundaries must be
midnight UTC
+ // (hour = 00). If the scheduler incorrectly used session
timezone
+ // (Asia/Shanghai, UTC+8), the hour would be 16 instead of 00.
+ String lowerHour = lowerStr.substring(11, 13);
+ Assert.assertEquals("Lower bound hour should be 00 (midnight
UTC), proving"
+ + " dynamic_partition.time_zone was used: " + lowerStr,
+ "00", lowerHour);
+ }
+
+ for (Partition partition : table.getPartitions()) {
+ RangePartitionItem item = (RangePartitionItem)
partitionInfo.getItem(partition.getId());
+ Assert.assertNotNull("Each partition should have a range
item", item);
+ }
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void testTimeStampTzDynamicPartitionWeekUnit() throws Exception {
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("Europe/London");
+
+ String createOlapTblStmt = "CREATE TABLE
test.`timestamptz_dynamic_week` (\n"
+ + " `k1` TIMESTAMPTZ NULL COMMENT \"\",\n"
+ + " `k2` int NULL COMMENT \"\"\n"
+ + ") ENGINE=OLAP\n"
+ + "DUPLICATE KEY(`k1`, `k2`)\n"
+ + "PARTITION BY RANGE(`k1`)\n"
+ + "()\n"
+ + "DISTRIBUTED BY HASH(`k2`) BUCKETS 3\n"
+ + "PROPERTIES (\n"
+ + "\"replication_num\" = \"1\",\n"
+ + "\"dynamic_partition.enable\" = \"true\",\n"
+ + "\"dynamic_partition.start\" = \"-3\",\n"
+ + "\"dynamic_partition.end\" = \"3\",\n"
+ + "\"dynamic_partition.create_history_partition\" =
\"true\",\n"
+ + "\"dynamic_partition.time_unit\" = \"week\",\n"
+ + "\"dynamic_partition.prefix\" = \"p\",\n"
+ + "\"dynamic_partition.buckets\" = \"1\",\n"
+ + "\"dynamic_partition.time_zone\" = \"Asia/Tokyo\"\n"
+ + ");";
+ createTable(createOlapTblStmt);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+ OlapTable table = (OlapTable)
db.getTableOrAnalysisException("timestamptz_dynamic_week");
+ Assert.assertTrue(table.dynamicPartitionExists());
+
+ Env.getCurrentEnv().getDynamicPartitionScheduler()
+ .executeDynamicPartitionFirstTime(db.getId(),
table.getId());
+
+ int partitionCount = table.getPartitionNames().size();
+ Assert.assertEquals(7, partitionCount);
+
+ // Verify partition names follow week pattern (clean, no timezone
suffix)
+ RangePartitionInfo partitionInfo = (RangePartitionInfo)
table.getPartitionInfo();
+ for (Map.Entry<Long, PartitionItem> entry :
partitionInfo.getIdToItem(false).entrySet()) {
+ RangePartitionItem item = (RangePartitionItem)
entry.getValue();
+ String partitionName =
table.getPartition(entry.getKey()).getName();
+ Assert.assertTrue("Partition name should start with 'p': " +
partitionName,
+ partitionName.startsWith("p"));
+ // Week partition name should be like "p2026_26" (year_week)
+ Assert.assertFalse("Partition name must not contain timezone:
" + partitionName,
+ partitionName.contains("Asia") ||
partitionName.contains("Tokyo"));
+
+ // Verify range validity
+ Range<PartitionKey> range = item.getItems();
+ Assert.assertTrue("lower must be < upper",
+ range.lowerEndpoint().compareTo(range.upperEndpoint())
< 0);
+ }
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void testTimeStampTzDynamicPartitionHourUnit() throws Exception {
+ String originalTimeZone =
connectContext.getSessionVariable().getTimeZone();
+ try {
+ connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+ String createOlapTblStmt = "CREATE TABLE
test.`timestamptz_dynamic_hour` (\n"
+ + " `k1` TIMESTAMPTZ NULL COMMENT \"\",\n"
+ + " `k2` int NULL COMMENT \"\"\n"
+ + ") ENGINE=OLAP\n"
+ + "DUPLICATE KEY(`k1`, `k2`)\n"
+ + "PARTITION BY RANGE(`k1`)\n"
+ + "()\n"
+ + "DISTRIBUTED BY HASH(`k2`) BUCKETS 3\n"
+ + "PROPERTIES (\n"
+ + "\"replication_num\" = \"1\",\n"
+ + "\"dynamic_partition.enable\" = \"true\",\n"
+ + "\"dynamic_partition.start\" = \"-3\",\n"
+ + "\"dynamic_partition.end\" = \"3\",\n"
+ + "\"dynamic_partition.create_history_partition\" =
\"true\",\n"
+ + "\"dynamic_partition.time_unit\" = \"hour\",\n"
+ + "\"dynamic_partition.prefix\" = \"p\",\n"
+ + "\"dynamic_partition.buckets\" = \"1\",\n"
+ + "\"dynamic_partition.time_zone\" = \"+00:00\"\n"
+ + ");";
+ createTable(createOlapTblStmt);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+ OlapTable table = (OlapTable)
db.getTableOrAnalysisException("timestamptz_dynamic_hour");
+ Assert.assertTrue(table.dynamicPartitionExists());
+
+ Env.getCurrentEnv().getDynamicPartitionScheduler()
+ .executeDynamicPartitionFirstTime(db.getId(),
table.getId());
+
+ int partitionCount = table.getPartitionNames().size();
+ Assert.assertEquals(7, partitionCount);
+
+ // Hour partition names are "p" + yyyyMMddHH (10 chars)
+ RangePartitionInfo partitionInfo = (RangePartitionInfo)
table.getPartitionInfo();
+ for (Map.Entry<Long, PartitionItem> entry :
partitionInfo.getIdToItem(false).entrySet()) {
+ RangePartitionItem item = (RangePartitionItem)
entry.getValue();
+ String partitionName =
table.getPartition(entry.getKey()).getName();
+ Assert.assertTrue("Partition name should start with 'p': " +
partitionName,
+ partitionName.startsWith("p"));
+ // Hour partition names: p + yyyyMMddHH → length 11 (p + 10
digits)
+ Assert.assertEquals("Hour partition name length: " +
partitionName,
+ 11, partitionName.length());
+
+ // Verify range validity and UTC boundaries
+ Range<PartitionKey> range = item.getItems();
+ Assert.assertTrue("lower must be < upper",
+ range.lowerEndpoint().compareTo(range.upperEndpoint())
< 0);
+
+ // With time_zone = "+00:00", partition boundaries should be
UTC timestamps
+ List<LiteralExpr> lowerKeys = range.lowerEndpoint().getKeys();
+ Assert.assertEquals(1, lowerKeys.size());
+ String lowerStr = lowerKeys.get(0).getStringValue();
+ Assert.assertTrue("Lower key must have +00:00 suffix: " +
lowerStr,
+ lowerStr.contains("+00:00"));
+ }
+ } finally {
+ connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+ }
+ }
+
+ @Test
+ public void testAutoPartitionRetentionTimestampTzCutoffNormalized() throws
Exception {
+ // The bug occurs on the scheduler thread where there is no
ConnectContext:
+ // DateUtils.getTimeZone() falls back to JVM default (e.g.
Asia/Shanghai)
+ // while TimestampTzLiteral.fromSessionTimeZone() falls back to UTC.
+ // This creates a mismatch where currentTimeStr is formatted in the JVM
+ // timezone but parsed as UTC, shifting the cutoff.
+ String originalSessionTz =
connectContext.getSessionVariable().getTimeZone();
+ ConnectContext savedCtx = ConnectContext.get();
+ TimeZone originalJvmTz = TimeZone.getDefault();
+ try {
+ // Create table and add partitions — use session TZ for table ops.
+ connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+
+ String createSql = "CREATE TABLE test.`auto_retention_tstz` (\n"
+ + " `k1` TIMESTAMPTZ NOT NULL\n"
+ + ") ENGINE=OLAP\n"
+ + "DUPLICATE KEY(`k1`)\n"
+ + "AUTO PARTITION BY RANGE (date_trunc(k1, 'day')) ()\n"
+ + "DISTRIBUTED BY HASH(`k1`) BUCKETS 1\n"
+ + "PROPERTIES (\n"
+ + "\"replication_num\" = \"1\",\n"
+ + "\"partition.retention_count\" = \"1\"\n"
+ + ");";
+ createTable(createSql);
+
+ Database db =
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+ OlapTable tbl = (OlapTable)
db.getTableOrAnalysisException("auto_retention_tstz");
+
+ // Definitively historical partition (year 2000) — always history.
+ alterTable("ALTER TABLE test.auto_retention_tstz ADD PARTITION
p_old VALUES "
+ + "[('2000-01-01 00:00:00+00:00'), ('2000-01-02
00:00:00+00:00'))");
+ // Another historical partition (year 2020) — always history.
+ alterTable("ALTER TABLE test.auto_retention_tstz ADD PARTITION
p_mid VALUES "
+ + "[('2020-01-01 00:00:00+00:00'), ('2020-01-02
00:00:00+00:00'))");
+
+ // Recent partition: upper bound is 4h in the future (UTC).
+ DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd
HH:mm:ss");
+ ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
+ String recentLower = utcNow.minusHours(1).format(fmt) + "+00:00";
+ String recentUpper = utcNow.plusHours(4).format(fmt) + "+00:00";
+ alterTable("ALTER TABLE test.auto_retention_tstz ADD PARTITION
p_recent VALUES "
+ + "[('" + recentLower + "'), ('" + recentUpper + "'))");
+ Assert.assertEquals(3, tbl.getPartitionNames().size());
+
+ // Simulate the scheduler thread: remove ConnectContext and set
+ // JVM default to a non-UTC zone. DateUtils.getTimeZone() now
+ // returns Asia/Shanghai while TimestampTzLiteral parsing defaults
+ // to UTC — the exact mismatch this fix protects against.
+ ConnectContext.remove();
+ TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
+ try {
+ Env.getCurrentEnv().getDynamicPartitionScheduler()
+ .executeDynamicPartitionFirstTime(db.getId(),
tbl.getId());
+ } finally {
+ TimeZone.setDefault(originalJvmTz);
+ if (savedCtx != null) {
+ savedCtx.setThreadLocalInfo();
+ }
+ }
+
+ // With the fix, cutoff is UTC-normalized: p_recent (upper bound
+ // ahead of UTC now) is not history → kept.
+ // p_old (2000, oldest history) → dropped by retention_count=1.
+ // p_mid (2020, latest history) → kept.
+ // Total: 2 partitions survive.
+ Assert.assertEquals("After retention, 2 partitions should remain",
2,
+ tbl.getPartitionNames().size());
+ Assert.assertTrue("p_mid should be kept as the latest history
partition",
+ tbl.getPartitionNames().contains("p_mid"));
+ Assert.assertTrue("p_recent should survive (not history)",
+ tbl.getPartitionNames().contains("p_recent"));
+ } finally {
+ TimeZone.setDefault(originalJvmTz);
+ connectContext.getSessionVariable().setTimeZone(originalSessionTz);
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/PartitionKeyTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/PartitionKeyTest.java
index 1c936da18bc..b5148f982ea 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/PartitionKeyTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/PartitionKeyTest.java
@@ -17,9 +17,11 @@
package org.apache.doris.catalog;
+import org.apache.doris.analysis.DateLiteral;
import org.apache.doris.analysis.PartitionValue;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.FeConstants;
+import org.apache.doris.qe.ConnectContext;
import org.junit.Assert;
import org.junit.BeforeClass;
@@ -45,6 +47,7 @@ public class PartitionKeyTest {
private static Column largeInt;
private static Column date;
private static Column datetime;
+ private static Column timestampTz;
private static Column charString;
private static Column varchar;
private static Column bool;
@@ -63,6 +66,7 @@ public class PartitionKeyTest {
largeInt = new Column("largeint", PrimitiveType.LARGEINT);
date = new Column("date", PrimitiveType.DATE);
datetime = new Column("datetime", PrimitiveType.DATETIME);
+ timestampTz = new Column("timestamptz",
ScalarType.createTimeStampTzType(6), true, null, "", "");
charString = new Column("char", PrimitiveType.CHAR);
varchar = new Column("varchar", PrimitiveType.VARCHAR);
bool = new Column("bool", PrimitiveType.BOOLEAN);
@@ -287,4 +291,158 @@ public class PartitionKeyTest {
PartitionKey key = PartitionKey.createInfinityPartitionKey(allColumns,
true);
Assert.assertEquals("(MAXVALUE, MAXVALUE, MAXVALUE, MAXVALUE,
MAXVALUE, MAXVALUE, MAXVALUE)", key.toSql());
}
+
+ @Test
+ public void testTimestampTzPartitionKeyKeepsExplicitOffset() throws
Exception {
+ boolean originalRunningUnitTest = FeConstants.runningUnitTest;
+ FeConstants.runningUnitTest = true;
+ ConnectContext context = new ConnectContext();
+ context.setThreadLocalInfo();
+ try {
+ context.getSessionVariable().setTimeZone("America/New_York");
+
+ PartitionKey key = PartitionKey.createPartitionKey(
+ Arrays.asList(new PartitionValue("2024-01-15 12:00:00
+00:00")),
+ Arrays.asList(timestampTz));
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertEquals(0, literal.getMicrosecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ } finally {
+ ConnectContext.remove();
+ FeConstants.runningUnitTest = originalRunningUnitTest;
+ }
+ }
+
+ @Test
+ public void testTimestampTzPartitionKeyAcceptsNamedTimezone() throws
Exception {
+ PartitionKey key = PartitionKey.createPartitionKey(
+ Arrays.asList(new PartitionValue("2024-01-15
20:00:00Asia/Shanghai")),
+ Arrays.asList(timestampTz));
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertEquals(0, literal.getMicrosecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ }
+
+ @Test
+ public void testTimestampTzPartitionKeyAcceptsLowercaseTimezone() throws
Exception {
+ PartitionKey key = PartitionKey.createPartitionKey(
+ Arrays.asList(new PartitionValue("2024-01-15 12:00:00
uTc")),
+ Arrays.asList(timestampTz));
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertEquals(0, literal.getMicrosecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ }
+
+ @Test
+ public void
testTimestampTzPartitionKeyUsesSessionTimezoneWithoutExplicitOffset() throws
Exception {
+ boolean originalRunningUnitTest = FeConstants.runningUnitTest;
+ FeConstants.runningUnitTest = true;
+ ConnectContext context = new ConnectContext();
+ context.setThreadLocalInfo();
+ try {
+ context.getSessionVariable().setTimeZone("America/New_York");
+
+ PartitionKey key = PartitionKey.createPartitionKey(
+ Arrays.asList(new PartitionValue("2024-01-15 12:00:00")),
+ Arrays.asList(timestampTz));
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(17, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertEquals(0, literal.getMicrosecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
17:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ } finally {
+ ConnectContext.remove();
+ FeConstants.runningUnitTest = originalRunningUnitTest;
+ }
+ }
+
+ @Test
+ public void testListTimestampTzPartitionKeyAcceptsNamedTimezone() throws
Exception {
+ // LIST path: createListPartitionKeyWithTypes must parse named
timezones
+ // the same way createPartitionKey does, routing through
TimestampTzLiteral.
+ PartitionKey key = PartitionKey.createListPartitionKeyWithTypes(
+ Arrays.asList(new PartitionValue("2024-01-15
20:00:00Asia/Shanghai")),
+ Arrays.asList((Type) ScalarType.createTimeStampTzType(6)),
+ false);
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ // Asia/Shanghai (UTC+8) → 20:00 - 8h = 12:00 UTC
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ }
+
+ @Test
+ public void testListTimestampTzPartitionKeyAcceptsLowercaseTimezone()
throws Exception {
+ // LIST path: lowercase timezone names like "uTc" must be recognized.
+ PartitionKey key = PartitionKey.createListPartitionKeyWithTypes(
+ Arrays.asList(new PartitionValue("2024-01-15 12:00:00 uTc")),
+ Arrays.asList((Type) ScalarType.createTimeStampTzType(6)),
+ false);
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ // uTc = UTC → no offset change
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ }
+
+ @Test
+ public void testListTimestampTzPartitionKeyKeepsExplicitOffset() throws
Exception {
+ // LIST path: explicit +00:00 offset must be preserved as UTC.
+ PartitionKey key = PartitionKey.createListPartitionKeyWithTypes(
+ Arrays.asList(new PartitionValue("2024-01-15 12:00:00+00:00")),
+ Arrays.asList((Type) ScalarType.createTimeStampTzType(6)),
+ false);
+
+ DateLiteral literal = (DateLiteral) key.getKeys().get(0);
+ Assert.assertEquals(2024, literal.getYear());
+ Assert.assertEquals(1, literal.getMonth());
+ Assert.assertEquals(15, literal.getDay());
+ Assert.assertEquals(12, literal.getHour());
+ Assert.assertEquals(0, literal.getMinute());
+ Assert.assertEquals(0, literal.getSecond());
+ Assert.assertTrue(literal.getStringValue().startsWith("2024-01-15
12:00:00"));
+ Assert.assertTrue(literal.getStringValue().endsWith("+00:00"));
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescRollUpGeneratorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescRollUpGeneratorTest.java
index 388c11e6aee..7fdcde6a3cc 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescRollUpGeneratorTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/mtmv/MTMVRelatedPartitionDescRollUpGeneratorTest.java
@@ -22,9 +22,13 @@ import org.apache.doris.analysis.PartitionKeyDesc;
import org.apache.doris.analysis.PartitionValue;
import org.apache.doris.analysis.SlotRef;
import org.apache.doris.analysis.StringLiteral;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.PartitionKey;
+import org.apache.doris.catalog.ScalarType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.mtmv.MTMVPartitionInfo.MTMVPartitionType;
+import org.apache.doris.qe.ConnectContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -116,4 +120,72 @@ public class MTMVRelatedPartitionDescRollUpGeneratorTest {
}
return PartitionKeyDesc.createIn(partitionValues);
}
+
+ @Test
+ public void testRollUpRangeTimestampTz() throws AnalysisException {
+ FunctionCallExpr expr = new FunctionCallExpr("date_trunc",
+ Lists.newArrayList(new SlotRef(null, null), new
StringLiteral("day")), true);
+ ConnectContext context = new ConnectContext();
+ context.getSessionVariable().setTimeZone("America/New_York");
+ context.setThreadLocalInfo();
+ try (MockedStatic<MTMVPartitionUtil> mtmvPartitionUtilStatic =
Mockito.mockStatic(MTMVPartitionUtil.class)) {
+ mtmvPartitionUtilStatic.when(() ->
MTMVPartitionUtil.getPartitionColumnType(
+ Mockito.nullable(MTMVRelatedTableIf.class),
Mockito.nullable(String.class)))
+ .thenReturn(ScalarType.createTimeStampTzType(6));
+ Mockito.when(mtmvPartitionInfo.getRelatedTable()).thenReturn(null);
+ Mockito.when(mtmvPartitionInfo.getExpr()).thenReturn(expr);
+
Mockito.when(mtmvPartitionInfo.getPartitionType()).thenReturn(MTMVPartitionType.EXPR);
+
+ MTMVRelatedPartitionDescRollUpGenerator generator = new
MTMVRelatedPartitionDescRollUpGenerator();
+ Map<PartitionKeyDesc, Set<String>> relatedPartitionDescs =
Maps.newHashMap();
+
+ // Two adjacent partitions within the same UTC day.
+ // With session tz America/New_York (UTC-5), DateTimeV2Literal
would
+ // convert +00:00 to NY local time, shifting the date-trunc result
to
+ // the previous day. TimestampTzLiteral preserves UTC so date_trunc
+ // correctly keeps both partitions in the same day.
+ PartitionKeyDesc desc1 = PartitionKeyDesc.createFixed(
+ Lists.newArrayList(new PartitionValue("2024-01-15
02:00:00.000000+00:00")),
+ Lists.newArrayList(new PartitionValue("2024-01-15
12:00:00.000000+00:00")));
+ PartitionKeyDesc desc2 = PartitionKeyDesc.createFixed(
+ Lists.newArrayList(new PartitionValue("2024-01-15
12:00:00.000000+00:00")),
+ Lists.newArrayList(new PartitionValue("2024-01-15
22:00:00.000000+00:00")));
+ relatedPartitionDescs.put(desc1, Sets.newHashSet("name1"));
+ relatedPartitionDescs.put(desc2, Sets.newHashSet("name2"));
+
+ Map<PartitionKeyDesc, Set<String>> res =
generator.rollUpRange(relatedPartitionDescs,
+ mtmvPartitionInfo, null);
+
+ // Both partitions should roll up to the same UTC day range.
+ // The +00:00 suffix ensures TimestampTzLiteral.fromSessionTimeZone
+ // treats the bounds as explicit UTC rather than session-local
time.
+ PartitionKeyDesc expectDesc = PartitionKeyDesc.createFixed(
+ Lists.newArrayList(new PartitionValue("2024-01-15
00:00:00+00:00")),
+ Lists.newArrayList(new PartitionValue("2024-01-16
00:00:00+00:00")));
+ Assert.assertEquals(1, res.size());
+ Assert.assertEquals(Sets.newHashSet("name1", "name2"),
res.get(expectDesc));
+
+ // Verify that the rolled-up PartitionKeyDesc produces correct UTC
partition keys
+ // regardless of session timezone (America/New_York = UTC-5).
+ // Without the +00:00 suffix,
TimestampTzLiteral.fromSessionTimeZone would
+ // interpret "2024-01-15 00:00:00" as session-local, shifting the
UTC bound
+ // to 2024-01-15 05:00:00+00:00.
+ List<Column> partitionColumns = Lists.newArrayList(
+ new Column("k1", ScalarType.createTimeStampTzType(6)));
+ PartitionKey lowKey = PartitionKey.createPartitionKey(
+ expectDesc.getLowerValues(), partitionColumns);
+ PartitionKey upperKey = PartitionKey.createPartitionKey(
+ expectDesc.getUpperValues(), partitionColumns);
+
+ // Both should be stored as midnight UTC, not shifted to
session-local time.
+ String lowKeyStr = lowKey.getKeys().get(0).getStringValue();
+ String upperKeyStr = upperKey.getKeys().get(0).getStringValue();
+ Assert.assertTrue("Lower bound should be 2024-01-15 midnight UTC,
but was: " + lowKeyStr,
+ lowKeyStr.startsWith("2024-01-15 00:00:00"));
+ Assert.assertTrue("Upper bound should be 2024-01-16 midnight UTC,
but was: " + upperKeyStr,
+ upperKeyStr.startsWith("2024-01-16 00:00:00"));
+ } finally {
+ ConnectContext.remove();
+ }
+ }
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteralTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteralTest.java
index 09d9051e6ad..b1b58520b20 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteralTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StringLikeLiteralTest.java
@@ -24,9 +24,14 @@ import org.apache.doris.nereids.types.DecimalV3Type;
import org.apache.doris.nereids.types.DoubleType;
import org.apache.doris.nereids.types.FloatType;
import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.TimeStampTzType;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
public class StringLikeLiteralTest {
@Test
@@ -198,4 +203,40 @@ public class StringLikeLiteralTest {
Assertions.assertThrows(CastException.class, () ->
finalS2.uncheckedCastTo(DecimalV3Type.createDecimalV3Type(6, 2)));
}
+
+ @Test
+ void testUncheckedCastToTimeStampTzRejectsUnstrictNoOffsetInStrictMode() {
+ try (MockedStatic<SessionVariable> mockedSessionVariable =
Mockito.mockStatic(SessionVariable.class)) {
+
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(true);
+
+ StringLiteral literal = new StringLiteral("2020-02-01 00-00-00");
+ Assertions.assertThrows(CastException.class,
+ () ->
literal.uncheckedCastTo(TimeStampTzType.SYSTEM_DEFAULT));
+ }
+ }
+
+ @Test
+ void testUncheckedCastToTimeStampTzUsesSessionTimeZoneWithoutOffset() {
+ try (MockedStatic<SessionVariable> mockedSessionVariable =
Mockito.mockStatic(SessionVariable.class)) {
+
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
+
+ ConnectContext context = new ConnectContext();
+ context.getSessionVariable().setTimeZone("America/New_York");
+ context.setThreadLocalInfo();
+ try {
+ TimestampTzLiteral literal = (TimestampTzLiteral) new
StringLiteral("2024-01-15 12:00:00")
+ .uncheckedCastTo(TimeStampTzType.of(6));
+
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(15, literal.day);
+ Assertions.assertEquals(17, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(0, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ } finally {
+ ConnectContext.remove();
+ }
+ }
+ }
}
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 3e000de8a82..ac5235fdf34 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
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.trees.expressions.literal;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.types.TimeStampTzType;
+import org.apache.doris.qe.ConnectContext;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -95,6 +96,36 @@ class TimestampTzLiteralTest {
Assertions.assertEquals(0, literal.microSecond);
}
+ @Test
+ void testFromSessionTimeZoneWithoutExplicitOffset() {
+ ConnectContext context = new ConnectContext();
+ context.getSessionVariable().setTimeZone("America/New_York");
+ context.setThreadLocalInfo();
+ try {
+ TimestampTzLiteral literal =
TimestampTzLiteral.fromSessionTimeZone(
+ TimeStampTzType.of(6), "2024-01-15 12:00:00");
+
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(15, literal.day);
+ Assertions.assertEquals(17, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(0, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ Assertions.assertEquals(6, literal.getDataType().getScale());
+ } finally {
+ ConnectContext.remove();
+ }
+ }
+
+ @Test
+ void testGetStringValueKeepsUtcSuffix() {
+ TimestampTzLiteral literal = new
TimestampTzLiteral(TimeStampTzType.of(6),
+ "2024-01-15 12:00:00 +00:00");
+
+ Assertions.assertEquals("2024-01-15 12:00:00.000000+00:00",
literal.getStringValue());
+ }
+
@Test
void testBasicArithmetic() {
TimestampTzLiteral base1 = new TimestampTzLiteral("2025-12-31
23:59:59.999999");
@@ -318,4 +349,95 @@ class TimestampTzLiteralTest {
Assertions.assertEquals(58, result.second);
Assertions.assertEquals(900000, result.microSecond);
}
+
+ @Test
+ void testRoundMicroSecondWithOverflow() {
+ // Scale=3: rounding 9995 → 10000, overflow microsecond to 1000000,
should carry 1 second
+ // This previously crashed with DateTimeParseException because
roundMicroSecond
+ // tried to reparse getStringValue() which includes a "+00:00" suffix.
+ TimestampTzLiteral literal = new TimestampTzLiteral(
+ TimeStampTzType.of(3), "2024-01-01 00:00:00.9995 +00:00");
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(1, literal.day);
+ Assertions.assertEquals(0, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(1, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ Assertions.assertEquals(3, literal.getDataType().getScale());
+ Assertions.assertEquals("2024-01-01 00:00:01.000+00:00",
literal.getStringValue());
+ }
+
+ @Test
+ void testRoundMicroSecondWithOverflowAndTimezone() {
+ // Rounding overflow with a non-UTC timezone string (parsed via
explicit zone path)
+ TimestampTzLiteral literal = new TimestampTzLiteral(
+ TimeStampTzType.of(3), "2024-01-01 00:00:00.9995
America/New_York");
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(1, literal.day);
+ Assertions.assertEquals(5, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(1, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ Assertions.assertEquals(3, literal.getDataType().getScale());
+ }
+
+ @Test
+ void testRoundMicroSecondWithoutOverflow() {
+ // Scale=3: rounding 9994 → 999, no overflow
+ TimestampTzLiteral literal = new TimestampTzLiteral(
+ TimeStampTzType.of(3), "2024-06-15 12:30:45.9994 +00:00");
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(6, literal.month);
+ Assertions.assertEquals(15, literal.day);
+ Assertions.assertEquals(12, literal.hour);
+ Assertions.assertEquals(30, literal.minute);
+ Assertions.assertEquals(45, literal.second);
+ // microSecond stores microseconds: 999ms = 999000µs
+ Assertions.assertEquals(999000, literal.microSecond);
+ }
+
+ @Test
+ void testRoundMicroSecondOverflowCrossDateBoundary() {
+ // Scale=3: 23:59:59.9995 → overflows to next day 00:00:00
+ TimestampTzLiteral literal = new TimestampTzLiteral(
+ TimeStampTzType.of(3), "2024-01-01 23:59:59.9995 +00:00");
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(2, literal.day);
+ Assertions.assertEquals(0, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(0, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ }
+
+ @Test
+ void testFromTimeZoneExplicit() {
+ // fromTimeZone with explicit timezone should convert from given tz to
UTC
+ TimestampTzLiteral literal = TimestampTzLiteral.fromTimeZone(
+ TimeStampTzType.of(6), "2024-01-15 00:00:00",
"America/New_York");
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(15, literal.day);
+ Assertions.assertEquals(5, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(0, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ }
+
+ @Test
+ void testFromTimeZoneExplicitWithOffsetString() {
+ // fromTimeZone with a string that already has a timezone should use
the string's timezone
+ TimestampTzLiteral literal = TimestampTzLiteral.fromTimeZone(
+ TimeStampTzType.of(6), "2024-01-15 00:00:00 +05:00",
"America/New_York");
+ // +05:00 in string should take precedence; 00:00:00 +05:00 →
2024-01-14 19:00:00 UTC
+ Assertions.assertEquals(2024, literal.year);
+ Assertions.assertEquals(1, literal.month);
+ Assertions.assertEquals(14, literal.day);
+ Assertions.assertEquals(19, literal.hour);
+ Assertions.assertEquals(0, literal.minute);
+ Assertions.assertEquals(0, literal.second);
+ Assertions.assertEquals(0, literal.microSecond);
+ }
}
diff --git
a/regression-test/data/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.out
b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.out
new file mode 100644
index 00000000000..18cbd9e8d53
--- /dev/null
+++
b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.out
@@ -0,0 +1,26 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !bug107_create_shift --
+2024-01-15 12:30:00.000000+00:00 1
+2024-01-15 13:30:00.000000+00:00 2
+
+-- !bug108_like --
+2024-01-15 12:30:00.000000+00:00 1
+2024-01-15 13:30:00.000000+00:00 2
+
+-- !bug110_src --
+2024-01-15 12:30:00.000000+00:00 1
+2024-01-15 13:30:00.000000+00:00 2
+
+-- !bug111_src --
+2024-01-15 17:30:00.000000+00:00 1
+2024-01-15 18:30:00.000000+00:00 2
+
+-- !bug113_direct_date_trunc_utc --
+2024-06-15 00:00:00.000000+00:00
+
+-- !bug113_auto_tz_range --
+2024-06-15 20:00:00.000000+00:00 1
+
+-- !bug114_named_lowercase_tz --
+2024-01-15 12:30:00.000000+00:00 1
+2024-01-15 13:30:00.000000+00:00 2
diff --git
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.groovy
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.groovy
new file mode 100644
index 00000000000..6fe272c2d0c
--- /dev/null
+++
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_partition_boundary_timezone.groovy
@@ -0,0 +1,257 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_timestamptz_partition_boundary_timezone") {
+ def dbName = "timestamptz_partition_boundary_timezone"
+ def createBoundary = "PARTITION p1 VALUES [('2024-01-15
12:00:00.000000+00:00'), ('2024-01-15 13:00:00.000000+00:00'))"
+ def createBoundary2 = "PARTITION p2 VALUES [('2024-01-15
13:00:00.000000+00:00'), ('2024-01-15 14:00:00.000000+00:00'))"
+
+ sql "DROP DATABASE IF EXISTS ${dbName}"
+ sql "CREATE DATABASE ${dbName}"
+ sql "USE ${dbName}"
+
+ sql "SET enable_nereids_planner = true"
+ sql "SET enable_fallback_to_original_planner = false"
+
+ sql "SET time_zone = 'America/New_York'"
+ sql "DROP TABLE IF EXISTS bug107_create_shift"
+ sql """
+ CREATE TABLE bug107_create_shift (
+ ts TIMESTAMPTZ(6) NOT NULL,
+ seq INT NOT NULL
+ )
+ UNIQUE KEY(ts)
+ PARTITION BY RANGE(ts) (
+ PARTITION p1 VALUES [('2024-01-15 12:00:00 +00:00'), ('2024-01-15
13:00:00 +00:00')),
+ PARTITION p2 VALUES [('2024-01-15 13:00:00 +00:00'), ('2024-01-15
14:00:00 +00:00'))
+ )
+ DISTRIBUTED BY HASH(ts) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+
+ def createShift = sql "SHOW CREATE TABLE bug107_create_shift"
+ assertTrue(createShift[0][1].contains(createBoundary))
+ assertTrue(createShift[0][1].contains(createBoundary2))
+
+ sql "SET time_zone = '+00:00'"
+ sql """
+ INSERT INTO bug107_create_shift VALUES
+ (CAST('2024-01-15 12:30:00 +00:00' AS TIMESTAMPTZ(6)), 1),
+ (CAST('2024-01-15 13:30:00 +00:00' AS TIMESTAMPTZ(6)), 2)
+ """
+
+ order_qt_bug107_create_shift """
+ SELECT CAST(ts AS STRING), seq
+ FROM bug107_create_shift
+ ORDER BY seq
+ """
+
+ sql "SET time_zone = '+00:00'"
+ sql "DROP TABLE IF EXISTS bug108_src"
+ sql """
+ CREATE TABLE bug108_src (
+ ts TIMESTAMPTZ(6) NOT NULL,
+ seq INT NOT NULL
+ )
+ UNIQUE KEY(ts)
+ PARTITION BY RANGE(ts) (
+ PARTITION p1 VALUES [('2024-01-15 12:00:00 +00:00'), ('2024-01-15
13:00:00 +00:00')),
+ PARTITION p2 VALUES [('2024-01-15 13:00:00 +00:00'), ('2024-01-15
14:00:00 +00:00'))
+ )
+ DISTRIBUTED BY HASH(ts) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+
+ def srcCreate = sql "SHOW CREATE TABLE bug108_src"
+ assertTrue(srcCreate[0][1].contains(createBoundary))
+ assertTrue(srcCreate[0][1].contains(createBoundary2))
+
+ sql "SET time_zone = 'Asia/Shanghai'"
+ sql "DROP TABLE IF EXISTS bug108_like"
+ sql "CREATE TABLE bug108_like LIKE bug108_src"
+
+ def likeCreate = sql "SHOW CREATE TABLE bug108_like"
+ assertTrue(likeCreate[0][1].contains(createBoundary))
+ assertTrue(likeCreate[0][1].contains(createBoundary2))
+
+ sql "SET time_zone = '+00:00'"
+ sql """
+ INSERT INTO bug108_like VALUES
+ (CAST('2024-01-15 12:30:00 +00:00' AS TIMESTAMPTZ(6)), 1),
+ (CAST('2024-01-15 13:30:00 +00:00' AS TIMESTAMPTZ(6)), 2)
+ """
+
+ order_qt_bug108_like """
+ SELECT CAST(ts AS STRING), seq
+ FROM bug108_like
+ ORDER BY seq
+ """
+
+ sql "SET time_zone = '+00:00'"
+ sql "DROP TABLE IF EXISTS bug110_src"
+ sql """
+ CREATE TABLE bug110_src (
+ ts TIMESTAMPTZ(6) NOT NULL,
+ seq INT NOT NULL
+ )
+ UNIQUE KEY(ts)
+ PARTITION BY RANGE(ts) (
+ PARTITION p0 VALUES LESS THAN ('2024-01-15 13:00:00 +00:00'),
+ PARTITION p1 VALUES LESS THAN ('2024-01-15 14:00:00 +00:00')
+ )
+ DISTRIBUTED BY HASH(ts) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+
+ def bug110Create = sql "SHOW CREATE TABLE bug110_src"
+ assertTrue(bug110Create[0][1].contains(
+ "PARTITION p0 VALUES [('0000-01-01 00:00:00.000000+00:00'),
('2024-01-15 13:00:00.000000+00:00'))"))
+ assertTrue(bug110Create[0][1].contains(
+ "PARTITION p1 VALUES [('2024-01-15 13:00:00.000000+00:00'),
('2024-01-15 14:00:00.000000+00:00'))"))
+
+ sql """
+ INSERT INTO bug110_src VALUES
+ (CAST('2024-01-15 12:30:00 +00:00' AS TIMESTAMPTZ(6)), 1),
+ (CAST('2024-01-15 13:30:00 +00:00' AS TIMESTAMPTZ(6)), 2)
+ """
+
+ order_qt_bug110_src """
+ SELECT CAST(ts AS STRING), seq
+ FROM bug110_src
+ ORDER BY seq
+ """
+
+ sql "SET time_zone = 'America/New_York'"
+ sql "DROP TABLE IF EXISTS bug111_src"
+ sql """
+ CREATE TABLE bug111_src (
+ ts TIMESTAMPTZ(6) NOT NULL,
+ seq INT NOT NULL
+ )
+ UNIQUE KEY(ts)
+ PARTITION BY RANGE(ts) (
+ PARTITION p0 VALUES LESS THAN ('2024-01-15 13:00:00'),
+ PARTITION p1 VALUES LESS THAN ('2024-01-15 14:00:00')
+ )
+ DISTRIBUTED BY HASH(ts) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+
+ def bug111Create = sql "SHOW CREATE TABLE bug111_src"
+ assertTrue(bug111Create[0][1].contains(
+ "PARTITION p0 VALUES [('0000-01-01 00:00:00.000000+00:00'),
('2024-01-15 18:00:00.000000+00:00'))"))
+ assertTrue(bug111Create[0][1].contains(
+ "PARTITION p1 VALUES [('2024-01-15 18:00:00.000000+00:00'),
('2024-01-15 19:00:00.000000+00:00'))"))
+
+ sql "SET time_zone = '+00:00'"
+ sql """
+ INSERT INTO bug111_src VALUES
+ (CAST('2024-01-15 17:30:00 +00:00' AS TIMESTAMPTZ(6)), 1),
+ (CAST('2024-01-15 18:30:00 +00:00' AS TIMESTAMPTZ(6)), 2)
+ """
+
+ order_qt_bug111_src """
+ SELECT CAST(ts AS STRING), seq
+ FROM bug111_src
+ ORDER BY seq
+ """
+
+ sql "SET time_zone = '+00:00'"
+ sql "DROP TABLE IF EXISTS bug113_auto_tz_range"
+ sql """
+ CREATE TABLE bug113_auto_tz_range (
+ id INT,
+ ts_tz TIMESTAMPTZ(6) NOT NULL
+ )
+ DUPLICATE KEY(id)
+ AUTO PARTITION BY RANGE (date_trunc(`ts_tz`, 'day')) ()
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1"
+ )
+ """
+
+ order_qt_bug113_direct_date_trunc_utc """
+ SELECT CAST(date_trunc(ts, 'day') AS VARCHAR(64))
+ FROM (
+ SELECT CAST('2024-06-15 20:00:00 +00:00' AS TIMESTAMPTZ(6)) AS ts
+ ) t
+ """
+
+ sql """
+ INSERT INTO bug113_auto_tz_range VALUES
+ (1, CAST('2024-06-15 20:00:00 +00:00' AS TIMESTAMPTZ(6)))
+ """
+
+ def bug113Create = sql "SHOW CREATE TABLE bug113_auto_tz_range"
+ assertTrue(bug113Create[0][1].contains(
+ "PARTITION p20240615000000 VALUES [('2024-06-15
00:00:00.000000+00:00'), ('2024-06-16 00:00:00.000000+00:00'))"))
+
+ order_qt_bug113_auto_tz_range """
+ SELECT CAST(ts_tz AS STRING), id
+ FROM bug113_auto_tz_range
+ ORDER BY id
+ """
+
+ sql "SET time_zone = 'America/New_York'"
+ sql "DROP TABLE IF EXISTS bug114_named_lowercase_tz"
+ sql """
+ CREATE TABLE bug114_named_lowercase_tz (
+ ts TIMESTAMPTZ(6) NOT NULL,
+ seq INT NOT NULL
+ )
+ UNIQUE KEY(ts)
+ PARTITION BY RANGE(ts) (
+ PARTITION p1 VALUES [('2024-01-15 20:00:00Asia/Shanghai'),
('2024-01-15 13:00:00 uTc')),
+ PARTITION p2 VALUES [('2024-01-15 13:00:00 uTc'), ('2024-01-15
22:00:00 Asia/Shanghai'))
+ )
+ DISTRIBUTED BY HASH(ts) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true"
+ )
+ """
+
+ def bug114Create = sql "SHOW CREATE TABLE bug114_named_lowercase_tz"
+ assertTrue(bug114Create[0][1].contains(createBoundary))
+ assertTrue(bug114Create[0][1].contains(createBoundary2))
+
+ sql "SET time_zone = '+00:00'"
+ sql """
+ INSERT INTO bug114_named_lowercase_tz VALUES
+ (CAST('2024-01-15 12:30:00 +00:00' AS TIMESTAMPTZ(6)), 1),
+ (CAST('2024-01-15 13:30:00 +00:00' AS TIMESTAMPTZ(6)), 2)
+ """
+
+ order_qt_bug114_named_lowercase_tz """
+ SELECT CAST(ts AS STRING), seq
+ FROM bug114_named_lowercase_tz
+ ORDER BY seq
+ """
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]