github-actions[bot] commented on code in PR #65219:
URL: https://github.com/apache/doris/pull/65219#discussion_r3627398158


##########
fe/fe-core/src/test/java/org/apache/doris/catalog/DynamicPartitionTableTest.java:
##########
@@ -2313,4 +2570,1139 @@ public void 
testAutoPartitionRetentionTimestampTzCutoffNormalized() throws Excep
             connectContext.getSessionVariable().setTimeZone(originalSessionTz);
         }
     }
+
+    @Test
+    public void testTimeStampTzGetHistoricalPartitionsRangeBased() throws 
Exception {
+        // Verify that getHistoricalPartitions correctly identifies the current
+        // partition by range lower bound (currentUtcBorder) rather than by 
name.
+        // When the configured timezone (e.g. Asia/Shanghai) is a day ahead of 
UTC,
+        // the scheduler computes nowPartitionName from the TZ date (e.g. 
"p20260707")
+        // while currentUtcBorder is from the UTC date ("2026-07-06 
00:00:00+00:00").
+        // Name-based comparison would fail to exclude the real current 
partition
+        // (p20260706), treating it as historical. Range-based comparison 
correctly
+        // excludes it by matching the stored partition lower bound.
+
+        String createSql = "CREATE TABLE test.`tstz_hist_parts` (\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\" = \"Asia/Shanghai\"\n"
+                + ");";
+        createTable(createSql);
+
+        Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+        OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_hist_parts");
+
+        Env.getCurrentEnv().getDynamicPartitionScheduler()
+                .executeDynamicPartitionFirstTime(db.getId(), table.getId());
+
+        int totalPartitions = table.getPartitionNames().size();
+        Assert.assertEquals(7, totalPartitions);
+
+        RangePartitionInfo info = (RangePartitionInfo) 
table.getPartitionInfo();
+        DynamicPartitionProperty prop = 
table.getTableProperty().getDynamicPartitionProperty();
+        String partitionFormat = DynamicPartitionUtil.getPartitionFormat(
+                info.getPartitionColumns().get(0));
+        ZonedDateTime utcNow = ZonedDateTime.now(ZoneOffset.UTC);
+        // getPartitionRangeString appends +00:00 when current is UTC-based.
+        String currentUtcBorder = 
DynamicPartitionUtil.getPartitionRangeString(prop, utcNow, 0, partitionFormat);
+
+        // Use a deliberately mismatched nowPartitionName to simulate the 
scenario
+        // where the configured TZ and UTC disagree on the calendar date.
+        // Name-based comparison would NOT exclude any partition with this 
name.
+        String wrongNowPartitionName = "p_nonexistent";
+
+        // 1. Without currentUtcBorder: name-based cannot identify the current 
partition.
+        List<Partition> historicalNoUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                table, wrongNowPartitionName, null);
+        boolean currentFoundByName = false;
+        for (Partition p : historicalNoUtc) {
+            RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+            String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+            if (lowerBound.equals(currentUtcBorder)) {
+                currentFoundByName = true;
+                break;
+            }
+        }
+        Assert.assertTrue("Name-based should fail to exclude current 
partition: "
+                + "nowPartitionName='" + wrongNowPartitionName + "' does not 
match any partition",
+                currentFoundByName);
+
+        // 2. With currentUtcBorder: range-based correctly excludes the 
current partition.
+        List<Partition> historicalWithUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                table, wrongNowPartitionName, currentUtcBorder);
+        boolean currentFoundByRange = false;
+        for (Partition p : historicalWithUtc) {
+            RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+            String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+            if (lowerBound.equals(currentUtcBorder)) {
+                currentFoundByRange = true;
+                break;
+            }
+        }
+        Assert.assertFalse("Range-based should correctly exclude the current 
partition",
+                currentFoundByRange);
+        Assert.assertEquals("Should exclude exactly the current partition",
+                totalPartitions - 1, historicalWithUtc.size());
+    }
+
+    @Test
+    public void testTimeStampTzDynamicPartitionMonthUnit() throws Exception {
+        // Verify TIMESTAMPTZ dynamic partition with MONTH unit:
+        // boundaries are at UTC midnight (day=01 00:00:00+00:00),
+        // names use the configured timezone's calendar month.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+            String createOlapTblStmt = "CREATE TABLE 
test.`timestamptz_dynamic_month` (\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\" = \"month\",\n"
+                    + "\"dynamic_partition.prefix\" = \"p\",\n"
+                    + "\"dynamic_partition.buckets\" = \"1\",\n"
+                    + "\"dynamic_partition.time_zone\" = \"Asia/Shanghai\"\n"
+                    + ");";
+            createTable(createOlapTblStmt);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("timestamptz_dynamic_month");
+            Assert.assertTrue(table.dynamicPartitionExists());
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            int partitionCount = table.getPartitionNames().size();
+            Assert.assertEquals(7, partitionCount);
+
+            // Verify partition boundaries are UTC midnight and names use
+            // configured timezone (Asia/Shanghai).
+            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"));
+                // Month partition names: p + yyyyMM → length 7
+                Assert.assertEquals("Month partition name length: " + 
partitionName,
+                        7, partitionName.length());
+
+                // Verify range validity
+                Range<PartitionKey> range = item.getItems();
+                Assert.assertTrue("lower must be < upper",
+                        range.lowerEndpoint().compareTo(range.upperEndpoint()) 
< 0);
+
+                // Partition boundaries must be UTC timestamps with +00:00 
suffix.
+                List<LiteralExpr> lowerKeys = range.lowerEndpoint().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 = range.upperEndpoint().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"));
+
+                // Partition boundaries must be at UTC midnight (hour=00)
+                // regardless of time_zone. Day should be 01 (first of month).
+                String lowerDay = lowerStr.substring(8, 10);
+                Assert.assertEquals("Lower bound must be day 01 for month 
unit: " + lowerStr,
+                        "01", lowerDay);
+                String lowerHour = lowerStr.substring(11, 13);
+                Assert.assertEquals("Lower bound must be UTC midnight (00): " 
+ lowerStr,
+                        "00", lowerHour);
+                String upperHour = upperStr.substring(11, 13);
+                Assert.assertEquals("Upper bound must be UTC midnight (00): " 
+ upperStr,
+                        "00", upperHour);
+            }
+
+            // Identify the current partition (idx=0) by its stored range.
+            List<Map.Entry<Long, PartitionItem>> sorted = Lists.newArrayList(
+                    partitionInfo.getIdToItem(false).entrySet());
+            sorted.sort((a, b) -> {
+                RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+            });
+            Assert.assertEquals(7, sorted.size());
+            RangePartitionItem currentItem = (RangePartitionItem) 
sorted.get(3).getValue();
+            String currentLowerStr = 
currentItem.getItems().lowerEndpoint().getKeys().get(0)
+                    .getStringValue();
+            ZonedDateTime currentLower = ZonedDateTime.parse(currentLowerStr,
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"));
+            String expectedCurrentName = "p"
+                    + 
DateTimeFormatter.ofPattern("yyyyMM").format(currentLower);
+            String actualCurrentName = 
table.getPartition(sorted.get(3).getKey()).getName();
+            Assert.assertEquals("Current partition (idx=0) month name must 
match its UTC lower bound",
+                    expectedCurrentName, actualCurrentName);
+            Assert.assertEquals("Current partition lower bound must be UTC 
midnight",
+                    "00", currentLowerStr.substring(11, 13));
+            Assert.assertEquals("Current partition lower bound day must be 01",
+                    "01", currentLowerStr.substring(8, 10));
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzDynamicPartitionYearUnit() throws Exception {
+        // Verify TIMESTAMPTZ dynamic partition with YEAR unit:
+        // boundaries are at UTC midnight (month=01, day=01 00:00:00+00:00),
+        // names use the configured timezone's calendar year.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+            String createOlapTblStmt = "CREATE TABLE 
test.`timestamptz_dynamic_year` (\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\" = \"year\",\n"
+                    + "\"dynamic_partition.prefix\" = \"p\",\n"
+                    + "\"dynamic_partition.buckets\" = \"1\",\n"
+                    + "\"dynamic_partition.time_zone\" = \"Asia/Shanghai\"\n"
+                    + ");";
+            createTable(createOlapTblStmt);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("timestamptz_dynamic_year");
+            Assert.assertTrue(table.dynamicPartitionExists());
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            int partitionCount = table.getPartitionNames().size();
+            Assert.assertEquals(7, partitionCount);
+
+            // Verify partition boundaries are UTC midnight and names use
+            // configured timezone (Asia/Shanghai).
+            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"));
+                // Year partition names: p + yyyy → length 5
+                Assert.assertEquals("Year partition name length: " + 
partitionName,
+                        5, partitionName.length());
+
+                // Verify range validity
+                Range<PartitionKey> range = item.getItems();
+                Assert.assertTrue("lower must be < upper",
+                        range.lowerEndpoint().compareTo(range.upperEndpoint()) 
< 0);
+
+                // Partition boundaries must be UTC timestamps with +00:00 
suffix.
+                List<LiteralExpr> lowerKeys = range.lowerEndpoint().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 = range.upperEndpoint().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"));
+
+                // Partition boundaries must be at UTC midnight (hour=00)
+                // regardless of time_zone. Month should be 01, day should be 
01.
+                String lowerMonth = lowerStr.substring(5, 7);
+                Assert.assertEquals("Lower bound must be month 01 for year 
unit: " + lowerStr,
+                        "01", lowerMonth);
+                String lowerDay = lowerStr.substring(8, 10);
+                Assert.assertEquals("Lower bound must be day 01 for year unit: 
" + lowerStr,
+                        "01", lowerDay);
+                String lowerHour = lowerStr.substring(11, 13);
+                Assert.assertEquals("Lower bound must be UTC midnight (00): " 
+ lowerStr,
+                        "00", lowerHour);
+                String upperHour = upperStr.substring(11, 13);
+                Assert.assertEquals("Upper bound must be UTC midnight (00): " 
+ upperStr,
+                        "00", upperHour);
+            }
+
+            // Identify the current partition (idx=0) by its stored range.
+            List<Map.Entry<Long, PartitionItem>> sorted = Lists.newArrayList(
+                    partitionInfo.getIdToItem(false).entrySet());
+            sorted.sort((a, b) -> {
+                RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+            });
+            Assert.assertEquals(7, sorted.size());
+            RangePartitionItem currentItem = (RangePartitionItem) 
sorted.get(3).getValue();
+            String currentLowerStr = 
currentItem.getItems().lowerEndpoint().getKeys().get(0)
+                    .getStringValue();
+            ZonedDateTime currentLower = ZonedDateTime.parse(currentLowerStr,
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"));
+            String expectedCurrentName = "p"
+                    + DateTimeFormatter.ofPattern("yyyy").format(currentLower);
+            String actualCurrentName = 
table.getPartition(sorted.get(3).getKey()).getName();
+            Assert.assertEquals("Current partition (idx=0) year name must 
match its UTC lower bound",
+                    expectedCurrentName, actualCurrentName);
+            Assert.assertEquals("Current partition lower bound must be UTC 
midnight",
+                    "00", currentLowerStr.substring(11, 13));
+            Assert.assertEquals("Current partition lower bound month must be 
01",
+                    "01", currentLowerStr.substring(5, 7));
+            Assert.assertEquals("Current partition lower bound day must be 01",
+                    "01", currentLowerStr.substring(8, 10));
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzReservedHistoryPeriodsUtcAligned() throws 
Exception {
+        // Verify that reserved_history_periods uses UTC-midnight boundaries
+        // (via borderTimeZone from getDropPartitionOpForDynamic) consistently
+        // with the main drop cutoff and add-partition boundaries.
+        // Without the fix, getClosedRange() hardcoded the configured timezone
+        // (e.g. Asia/Shanghai) for convertToUtcTimestamp(), shifting reserved
+        // ranges by the UTC offset and causing mismatches with UTC-aligned
+        // partition boundaries.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+            String createSql = "CREATE TABLE test.`tstz_reserved_hist` (\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\" = 
\"false\",\n"
+                    + "\"dynamic_partition.time_unit\" = \"day\",\n"
+                    + "\"dynamic_partition.prefix\" = \"p\",\n"
+                    + "\"dynamic_partition.buckets\" = \"1\",\n"
+                    + "\"dynamic_partition.time_zone\" = \"Asia/Shanghai\",\n"
+                    + "\"dynamic_partition.reserved_history_periods\" = 
\"[2019-06-01,2020-08-01]\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_reserved_hist");
+
+            // Add partitions manually — must temporarily disable dynamic 
partition.
+            alterTable("ALTER TABLE test.tstz_reserved_hist SET "
+                    + "('dynamic_partition.enable' = 'false')");
+            // p_202001: well inside both the fixed UTC range and the old
+            // Asia/Shanghai-shifted range — weak test, passes either way.
+            alterTable("ALTER TABLE test.tstz_reserved_hist ADD PARTITION 
p_202001 VALUES "
+                    + "[('2020-01-01 00:00:00+00:00'), ('2020-02-01 
00:00:00+00:00'))");
+            // p_old: before both ranges — dropped by the start=-3 cutoff
+            // regardless of the reserved-period interpretation.
+            alterTable("ALTER TABLE test.tstz_reserved_hist ADD PARTITION 
p_old VALUES "
+                    + "[('2018-01-01 00:00:00+00:00'), ('2018-02-01 
00:00:00+00:00'))");
+
+            // p_boundary: discriminating partition. Its lower bound
+            // (2020-07-31 17:00 UTC) is AFTER the old shifted upper bound
+            // (~2020-07-31 16:00 UTC) but BEFORE the corrected UTC upper
+            // bound (2020-08-01 00:00 UTC). Only the fix keeps it.
+            alterTable("ALTER TABLE test.tstz_reserved_hist ADD PARTITION 
p_boundary VALUES "
+                    + "[('2020-07-31 17:00:00+00:00'), ('2020-07-31 
18:00:00+00:00'))");
+
+            Assert.assertTrue("p_202001 should exist before scheduling",
+                    table.getPartitionNames().contains("p_202001"));
+            Assert.assertTrue("p_old should exist before scheduling",
+                    table.getPartitionNames().contains("p_old"));
+            Assert.assertTrue("p_boundary should exist before scheduling",
+                    table.getPartitionNames().contains("p_boundary"));
+
+            // Re-enable dynamic partition and run the scheduler.
+            alterTable("ALTER TABLE test.tstz_reserved_hist SET "
+                    + "('dynamic_partition.enable' = 'true')");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            // p_202001 falls within the reserved period in both old and new
+            // interpretations — kept regardless.
+            Assert.assertTrue("p_202001 should be kept",
+                    table.getPartitionNames().contains("p_202001"));
+            // p_old is before the start=-3 cutoff and outside the reserved
+            // period — dropped regardless.
+            Assert.assertFalse("p_old should be dropped",
+                    table.getPartitionNames().contains("p_old"));
+            // p_boundary is the discriminating case: only kept when the
+            // reserved period is interpreted in UTC rather than shifted by
+            // the configured timezone (Asia/Shanghai, UTC+8).
+            Assert.assertTrue("p_boundary should be kept — reserved period is 
UTC-aligned,"
+                    + " not shifted by the configured timezone",
+                    table.getPartitionNames().contains("p_boundary"));
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzHotPartitionCooldownUtcAligned() throws 
Exception {
+        // Verify that hot-partition cooldown times are UTC-midnight aligned
+        // and not shifted by the configured timezone (America/Chicago, UTC-5).
+        // Without the fix, setStorageMediumProperty() used nowTz (configured
+        // timezone) which could place the cooldown boundary on a different
+        // calendar day than the partition's UTC range.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+            changeBeDisk(TStorageMedium.SSD);
+
+            String createSql = "CREATE TABLE test.`tstz_hot_part_cooldown` (\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\" = 
\"America/Chicago\",\n"
+                    + "\"dynamic_partition.hot_partition_num\" = \"1\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_hot_part_cooldown");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            RangePartitionInfo partitionInfo = (RangePartitionInfo) 
table.getPartitionInfo();
+            // Sort partitions by lower bound.
+            List<Map.Entry<Long, PartitionItem>> sortedEntries = 
Lists.newArrayList(
+                    partitionInfo.getIdToItem(false).entrySet());
+            sortedEntries.sort((a, b) -> {
+                RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+            });
+
+            Assert.assertEquals(7, sortedEntries.size());
+
+            // Partitions idx=-3,-2,-1 are before the hot range → HDD.
+            // Partitions idx=0..3 (4 partitions) are within 
hot_partition_num=1:
+            //   - idx=0: hot (offset + hotPartitionNum = 0+1 = 1 > 0) → SSD
+            //   - idx=1,2,3: hot (offset + hotPartitionNum > 0) → SSD
+            // Because hot_partition_num=1 means only the current partition is 
hot,
+            // but the logic counts from idx + hotPartitionNum > 0.
+            // Actually: the condition is `offset + hotPartitionNum <= 0` → 
HDD.
+            // So idx=-3,-2,-1: -3+1=-2≤0 HDD, -2+1=-1≤0 HDD, -1+1=0≤0 HDD.
+            // idx=0,1,2,3: 0+1=1>0 SSD, etc.
+            // idx=-3,-2,-1 are HDD, idx=0,1,2,3 are SSD.
+
+            for (int i = 0; i < sortedEntries.size(); i++) {
+                Map.Entry<Long, PartitionItem> entry = sortedEntries.get(i);
+                RangePartitionItem item = (RangePartitionItem) 
entry.getValue();
+                DataProperty dp = 
partitionInfo.getDataProperty(entry.getKey());
+
+                if (i < 3) {
+                    // Historical partitions: HDD
+                    Assert.assertEquals("Historical partition should be HDD: 
idx=" + (i - 3),
+                            TStorageMedium.HDD, dp.getStorageMedium());
+                } else {
+                    // Hot partitions: SSD
+                    Assert.assertEquals("Hot partition should be SSD: idx=" + 
(i - 3),
+                            TStorageMedium.SSD, dp.getStorageMedium());
+
+                    // Every hot partition must have a finite cooldown equal to
+                    // that partition's upper endpoint (offset + 
hotPartitionNum).
+                    // Assert it is NOT the MAX fallback, which would indicate
+                    // the TIMESTAMPTZ lifecycle string was rejected during 
parse.
+                    Assert.assertNotEquals("Hot partition must have a finite 
cooldown: idx=" + (i - 3),
+                            DataProperty.MAX_COOLDOWN_TIME_MS, 
dp.getCooldownTimeMs());
+
+                    ZonedDateTime cooldownUtc = ZonedDateTime.ofInstant(
+                            
java.time.Instant.ofEpochMilli(dp.getCooldownTimeMs()),
+                            ZoneOffset.UTC);
+
+                    // Parse the partition's upper bound as a UTC instant.
+                    String upperStr = 
item.getItems().upperEndpoint().getKeys().get(0).getStringValue();
+                    ZonedDateTime upperUtc = ZonedDateTime.parse(upperStr,
+                            DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ssXXX"));
+
+                    Assert.assertEquals("Cooldown must equal the partition 
upper bound ("
+                            + upperUtc + "): idx=" + (i - 3),
+                            upperUtc.toInstant(), cooldownUtc.toInstant());
+                }
+
+                // Verify partition boundaries are UTC midnight.
+                List<LiteralExpr> lowerKeys = 
item.getItems().lowerEndpoint().getKeys();
+                String lowerStr = lowerKeys.get(0).getStringValue();
+                Assert.assertTrue("Lower key must be UTC: " + lowerStr,
+                        lowerStr.contains("+00:00"));
+                Assert.assertEquals("Lower bound must be at UTC midnight: " + 
lowerStr,
+                        "00", lowerStr.substring(11, 13));
+            }
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzHotPartitionCooldownDstMonth() throws Exception 
{
+        // Regression: When the session timezone has DST (e.g. America/Chicago,
+        // UTC-5 in summer, UTC-6 in winter), 
PropertyAnalyzer.analyzeDataProperty()
+        // parses cooldown values as DATETIME via 
DateLiteralUtils.createDateLiteral()
+        // which applies the current Instant's offset during the initial shift
+        // while using the target date's offset in unixTimestamp(). With the 
old
+        // convertToUtcTimestamp path (which appended "+00:00"), a cooldown
+        // boundary in a different DST period than NOW would shift by one hour.
+        // The fix stores the cooldown with an explicit +00:00 suffix, and
+        // PropertyAnalyzer now parses timezone-suffixed values directly as
+        // instants. This test uses MONTH unit with America/Chicago (DST) for
+        // both session and partition timezones so the cooldown boundary is
+        // likely to fall in a different DST period than the current date.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+            changeBeDisk(TStorageMedium.SSD);
+
+            String createSql = "CREATE TABLE test.`tstz_cooldown_dst_month` 
(\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\" = \"month\",\n"
+                    + "\"dynamic_partition.prefix\" = \"p\",\n"
+                    + "\"dynamic_partition.buckets\" = \"1\",\n"
+                    + "\"dynamic_partition.time_zone\" = 
\"America/Chicago\",\n"
+                    + "\"dynamic_partition.hot_partition_num\" = \"6\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_cooldown_dst_month");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            RangePartitionInfo partitionInfo = (RangePartitionInfo) 
table.getPartitionInfo();
+            List<Map.Entry<Long, PartitionItem>> sortedEntries = 
Lists.newArrayList(
+                    partitionInfo.getIdToItem(false).entrySet());
+            sortedEntries.sort((a, b) -> {
+                RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+            });
+
+            Assert.assertEquals(7, sortedEntries.size());
+
+            // idx=-3,-2,-1: offset+6=3,2,1 >0 → SSD (hot)
+            // idx=0..3: offset+6=6,7,8,9 >0 → SSD (hot)
+            // All partitions should be SSD since hot_partition_num covers all.
+
+            // Derive the expected cooldown from the current partition's
+            // stored lower bound rather than sampling ZonedDateTime.now(),
+            // which can differ from the clock captured by the scheduler
+            // and cause spurious failures across UTC month boundaries.
+            RangePartitionItem currentItem = (RangePartitionItem) 
sortedEntries.get(3).getValue();
+            ZonedDateTime currentLower = ZonedDateTime.parse(
+                    
currentItem.getItems().lowerEndpoint().getKeys().get(0).getStringValue(),
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"));
+
+            for (int i = 0; i < sortedEntries.size(); i++) {
+                int idx = i - 3; // idx=-3,-2,-1,0,1,2,3
+                Map.Entry<Long, PartitionItem> entry = sortedEntries.get(i);
+                RangePartitionItem item = (RangePartitionItem) 
entry.getValue();
+                DataProperty dp = 
partitionInfo.getDataProperty(entry.getKey());
+
+                Assert.assertEquals("All partitions should be SSD with 
hot_partition_num=6",
+                        TStorageMedium.SSD, dp.getStorageMedium());
+                Assert.assertNotEquals("Hot partition must have a finite 
cooldown",
+                        DataProperty.MAX_COOLDOWN_TIME_MS, 
dp.getCooldownTimeMs());
+
+                // Cooldown is the lower bound of the (idx + 
hotPartitionNum)-th
+                // partition. Derive it from the current partition's lower 
bound
+                // (which shares the scheduler's clock) to avoid race 
conditions.
+                ZonedDateTime cooldownUtc = ZonedDateTime.ofInstant(
+                        java.time.Instant.ofEpochMilli(dp.getCooldownTimeMs()),
+                        ZoneOffset.UTC);
+                ZonedDateTime expectedUtc = currentLower.plusMonths(idx + 6);
+                Assert.assertEquals("Cooldown must equal lower bound of 
partition at offset "
+                        + (idx + 6) + " (" + expectedUtc + "): idx=" + idx,
+                        expectedUtc.toInstant(), cooldownUtc.toInstant());
+
+                // Verify partition boundaries are UTC midnight, day=01.
+                List<LiteralExpr> lowerKeys = 
item.getItems().lowerEndpoint().getKeys();
+                String lowerStr = lowerKeys.get(0).getStringValue();
+                Assert.assertTrue("Lower key must be UTC: " + lowerStr,
+                        lowerStr.contains("+00:00"));
+                Assert.assertEquals("Lower bound must be at UTC midnight: " + 
lowerStr,
+                        "00", lowerStr.substring(11, 13));
+                // Month boundaries must fall on the first day of the month.
+                Assert.assertEquals("Month lower bound day must be 01: " + 
lowerStr,
+                        "01", lowerStr.substring(8, 10));
+            }
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzHotPartitionCooldownDstFallback() throws 
Exception {
+        // DST fall-back regression: on 2026-11-01 at 2:00 AM CDT, 
America/Chicago
+        // clocks fall back to 1:00 AM CST. The hour 01:00–02:00 occurs twice,
+        // so a wall-clock string "01:00:00" without offset is ambiguous and 
Java
+        // resolves it to the earlier offset (06:00 UTC instead of 07:00 UTC).
+        // The fix stores the cooldown as an unambiguous UTC timestamp with 
+00:00
+        // suffix (e.g. "2026-11-01 07:00:00+00:00"), and PropertyAnalyzer now
+        // parses timezone-suffixed values directly as instants, bypassing the
+        // broken DATETIME path. This test uses HOUR unit with America/Chicago
+        // to ensure cooldown timestamps remain correct regardless of DST 
status.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+            changeBeDisk(TStorageMedium.SSD);
+
+            String createSql = "CREATE TABLE test.`tstz_cooldown_dst_fallback` 
(\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\" = 
\"America/Chicago\",\n"
+                    + "\"dynamic_partition.hot_partition_num\" = \"1\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_cooldown_dst_fallback");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            RangePartitionInfo partitionInfo = (RangePartitionInfo) 
table.getPartitionInfo();
+            List<Map.Entry<Long, PartitionItem>> sortedEntries = 
Lists.newArrayList(
+                    partitionInfo.getIdToItem(false).entrySet());
+            sortedEntries.sort((a, b) -> {
+                RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+            });
+
+            Assert.assertEquals(7, sortedEntries.size());
+
+            // Derive the expected cooldown from the current partition's
+            // stored lower bound rather than sampling ZonedDateTime.now(),
+            // which can differ from the clock captured by the scheduler
+            // and cause spurious failures across UTC hour boundaries.
+            RangePartitionItem currentItem = (RangePartitionItem) 
sortedEntries.get(3).getValue();
+            ZonedDateTime currentLower = ZonedDateTime.parse(
+                    
currentItem.getItems().lowerEndpoint().getKeys().get(0).getStringValue(),
+                    DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssXXX"));
+
+            for (int i = 0; i < sortedEntries.size(); i++) {
+                int idx = i - 3;
+                Map.Entry<Long, PartitionItem> entry = sortedEntries.get(i);
+                RangePartitionItem item = (RangePartitionItem) 
entry.getValue();
+                DataProperty dp = 
partitionInfo.getDataProperty(entry.getKey());
+
+                // idx=-3,-2,-1: offset+1 ≤ 0 → HDD (MAX_COOLDOWN_TIME_MS)
+                // idx=0,1,2,3: offset+1 > 0 → SSD (finite cooldown)
+                if (idx + 1 <= 0) {
+                    Assert.assertEquals("Historical partition should be HDD: 
idx=" + idx,
+                            TStorageMedium.HDD, dp.getStorageMedium());
+                    continue;
+                }
+
+                Assert.assertEquals("Hot partition should be SSD: idx=" + idx,
+                        TStorageMedium.SSD, dp.getStorageMedium());
+                Assert.assertNotEquals("Hot partition must have a finite 
cooldown: idx=" + idx,
+                        DataProperty.MAX_COOLDOWN_TIME_MS, 
dp.getCooldownTimeMs());
+
+                // Cooldown is the lower bound of the (idx + 
hotPartitionNum)-th
+                // partition. Derive it from the current partition's lower 
bound
+                // (which shares the scheduler's clock) to avoid race 
conditions.
+                ZonedDateTime cooldownUtc = ZonedDateTime.ofInstant(
+                        java.time.Instant.ofEpochMilli(dp.getCooldownTimeMs()),
+                        ZoneOffset.UTC);
+                ZonedDateTime expectedUtc = currentLower.plusHours(idx + 1);
+                Assert.assertEquals("Cooldown must equal lower bound of 
partition at offset "
+                        + (idx + 1) + " (" + expectedUtc + "): idx=" + idx,
+                        expectedUtc.toInstant(), cooldownUtc.toInstant());
+
+                // Hour boundaries must be at whole UTC hours.
+                List<LiteralExpr> lowerKeys = 
item.getItems().lowerEndpoint().getKeys();
+                String lowerStr = lowerKeys.get(0).getStringValue();
+                Assert.assertTrue("Lower key must be UTC: " + lowerStr,
+                        lowerStr.contains("+00:00"));
+                int lowerHour = Integer.parseInt(lowerStr.substring(11, 13));
+                Assert.assertTrue("Lower bound hour must be 0-23: " + lowerStr,
+                        lowerHour >= 0 && lowerHour <= 23);
+            }
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzHotPartitionCooldownFractionalSeconds() throws 
Exception {
+        // Regression: convertToUtcTimestamp() preserves the column's scale
+        // (0–6) via TimeStampTzType.of(), so cooldown strings may carry
+        // fractional seconds (e.g. "2027-01-01 00:00:00.000000+00:00" for
+        // TIMESTAMPTZ(6)). The old PropertyAnalyzer branch used a fixed-
+        // position offset check and a "yyyy-MM-dd HH:mm:ssXXX" formatter,
+        // both of which silently rejected fractional-second values and fell
+        // back to MAX_COOLDOWN_TIME_MS. The fix uses a regex offset detector
+        // and a DateTimeFormatterBuilder with appendFraction(0,6,true) so
+        // values with any scale 0–6 are parsed as instants.
+        // This test exercises precisions 0, 3, and 6.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        int[] precisions = {0, 3, 6};
+        try {
+            connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+            changeBeDisk(TStorageMedium.SSD);
+
+            for (int precision : precisions) {
+                String tableName = "tstz_cooldown_frac_" + precision;
+                String createSql = "CREATE TABLE test.`" + tableName + "` (\n"
+                        + "  `k1` TIMESTAMPTZ(" + precision + ") 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\" = 
\"America/Chicago\",\n"
+                        + "\"dynamic_partition.hot_partition_num\" = \"1\"\n"
+                        + ");";
+                createTable(createSql);
+
+                Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+                OlapTable table = (OlapTable) 
db.getTableOrAnalysisException(tableName);
+
+                Env.getCurrentEnv().getDynamicPartitionScheduler()
+                        .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+                RangePartitionInfo partitionInfo = (RangePartitionInfo) 
table.getPartitionInfo();
+                List<Map.Entry<Long, PartitionItem>> sortedEntries = 
Lists.newArrayList(
+                        partitionInfo.getIdToItem(false).entrySet());
+                sortedEntries.sort((a, b) -> {
+                    RangePartitionItem ai = (RangePartitionItem) a.getValue();
+                    RangePartitionItem bi = (RangePartitionItem) b.getValue();
+                    return 
ai.getItems().lowerEndpoint().compareTo(bi.getItems().lowerEndpoint());
+                });
+
+                Assert.assertEquals("TIMESTAMPTZ(" + precision + "): partition 
count",
+                        7, sortedEntries.size());
+
+                // Hot partitions must have finite cooldown, not
+                // MAX_COOLDOWN_TIME_MS (which would mean the cooldown
+                // string was rejected by PropertyAnalyzer).
+                java.time.format.DateTimeFormatterBuilder fb =
+                        new java.time.format.DateTimeFormatterBuilder()
+                                .appendPattern("yyyy-MM-dd HH:mm:ss")
+                                
.appendFraction(java.time.temporal.ChronoField.NANO_OF_SECOND, 0, 6, true)
+                                .appendOffset("+HH:MM", "+00:00");
+                java.time.format.DateTimeFormatter boundFmt = fb.toFormatter();
+                for (int i = 0; i < sortedEntries.size(); i++) {
+                    int idx = i - 3;
+                    Map.Entry<Long, PartitionItem> entry = 
sortedEntries.get(i);
+                    RangePartitionItem item = (RangePartitionItem) 
entry.getValue();
+                    DataProperty dp = 
partitionInfo.getDataProperty(entry.getKey());
+
+                    if (i < 3) {
+                        Assert.assertEquals("TIMESTAMPTZ(" + precision
+                                + ") historical partition should be HDD: idx=" 
+ idx,
+                                TStorageMedium.HDD, dp.getStorageMedium());
+                    } else {
+                        Assert.assertEquals("TIMESTAMPTZ(" + precision
+                                + ") hot partition should be SSD: idx=" + idx,
+                                TStorageMedium.SSD, dp.getStorageMedium());
+                        Assert.assertNotEquals("TIMESTAMPTZ(" + precision
+                                + ") cooldown must be finite (not MAX): idx=" 
+ idx,
+                                DataProperty.MAX_COOLDOWN_TIME_MS, 
dp.getCooldownTimeMs());
+
+                        ZonedDateTime cooldownUtc = ZonedDateTime.ofInstant(
+                                
java.time.Instant.ofEpochMilli(dp.getCooldownTimeMs()),
+                                ZoneOffset.UTC);
+                        String upperStr = 
item.getItems().upperEndpoint().getKeys().get(0)
+                                .getStringValue();
+                        ZonedDateTime upperUtc = ZonedDateTime.parse(upperStr, 
boundFmt);
+                        Assert.assertEquals("TIMESTAMPTZ(" + precision
+                                + ") cooldown must equal partition upper 
bound: idx=" + idx,
+                                upperUtc.toInstant(), cooldownUtc.toInstant());
+                    }
+                }
+            }
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzGetHistoricalPartitionsScaledColumn() throws 
Exception {
+        // TIMESTAMPTZ(6) stores lower keys with ".000000+00:00" suffix while
+        // currentUtcBorder uses "+00:00" (no fractional part).  The old string
+        // comparison failed on this scale difference.  Fix: compare by
+        // PartitionKey.compareTo instead.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+            String createSql = "CREATE TABLE test.`tstz_hist_scaled` (\n"
+                    + "  `k1` TIMESTAMPTZ(6) 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\" = \"America/Chicago\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_hist_scaled");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            int totalPartitions = table.getPartitionNames().size();
+            Assert.assertEquals(7, totalPartitions);
+
+            RangePartitionInfo info = (RangePartitionInfo) 
table.getPartitionInfo();
+            // Verify that stored lower keys actually have .000000 fractional 
part.
+            for (PartitionItem item : info.getIdToItem(false).values()) {
+                RangePartitionItem rItem = (RangePartitionItem) item;
+                String lowerStr = 
rItem.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+                Assert.assertTrue("TIMESTAMPTZ(6) lower key must include 
.000000: " + lowerStr,
+                        lowerStr.contains(".000000"));
+            }
+
+            // Compute currentUtcBorder (no fractional seconds).
+            DynamicPartitionProperty prop = 
table.getTableProperty().getDynamicPartitionProperty();
+            String partitionFormat = DynamicPartitionUtil.getPartitionFormat(
+                    info.getPartitionColumns().get(0));
+            String currentUtcBorder = 
DynamicPartitionUtil.getPartitionRangeString(
+                    prop, ZonedDateTime.now(ZoneOffset.UTC), 0, 
partitionFormat);
+            Assert.assertFalse("currentUtcBorder should NOT contain fractional 
seconds: "
+                    + currentUtcBorder, currentUtcBorder.contains("."));
+
+            // Without currentUtcBorder: name-based cannot identify the current
+            // partition when nowPartitionName does not match.
+            String wrongNowPartitionName = "p_nonexistent";
+            List<Partition> historicalNoUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                    table, wrongNowPartitionName, null);
+            boolean currentFoundByName = false;
+            for (Partition p : historicalNoUtc) {
+                RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+                String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+                if (lowerBound.contains(currentUtcBorder.replace("+00:00", 
""))) {
+                    currentFoundByName = true;
+                    break;
+                }
+            }
+            Assert.assertTrue("Scaled TIMESTAMPTZ(6): name-based should fail 
to exclude current partition",
+                    currentFoundByName);
+
+            // With currentUtcBorder: range-based correctly excludes the 
current
+            // partition even though the string representations differ 
(.000000).
+            List<Partition> historicalWithUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                    table, wrongNowPartitionName, currentUtcBorder);
+            boolean currentFoundByRange = false;
+            for (Partition p : historicalWithUtc) {
+                RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+                String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+                if (lowerBound.contains(currentUtcBorder.replace("+00:00", 
""))) {
+                    currentFoundByRange = true;
+                    break;
+                }
+            }
+            Assert.assertFalse("Scaled TIMESTAMPTZ(6): range-based must 
exclude current partition",
+                    currentFoundByRange);
+            Assert.assertEquals("Scaled TIMESTAMPTZ(6): exclude exactly one 
partition",
+                    totalPartitions - 1, historicalWithUtc.size());
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzGetHistoricalPartitionsOldPrefix() throws 
Exception {
+        // When dynamic_partition.prefix is changed after initial partition
+        // creation, existing partitions keep their old names (e.g. 
"p20260720")
+        // while the new nowPartitionName uses the new prefix ("q20260720").
+        // Name-based comparison fails to identify the current partition;
+        // range-based comparison (currentUtcBorder) must still work.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("Asia/Shanghai");
+
+            String createSql = "CREATE TABLE test.`tstz_old_prefix` (\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\" = \"Asia/Shanghai\"\n"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_old_prefix");
+
+            Env.getCurrentEnv().getDynamicPartitionScheduler()
+                    .executeDynamicPartitionFirstTime(db.getId(), 
table.getId());
+
+            int totalPartitions = table.getPartitionNames().size();
+            Assert.assertEquals("Initial partitions should be 7", 7, 
totalPartitions);
+
+            // Verify all existing partitions use the old prefix "p".
+            for (String name : table.getPartitionNames()) {
+                Assert.assertTrue("Existing partitions must use old prefix: " 
+ name,
+                        name.startsWith("p"));
+            }
+
+            // Now change the prefix from "p" to "q" via table properties.
+            HashMap<String, String> newPrefixProps = new HashMap<>();
+            newPrefixProps.put("dynamic_partition.prefix", "q");
+            table.getTableProperty().modifyTableProperties(newPrefixProps);
+            table.getTableProperty().buildDynamicProperty();
+
+            // Re-read the dynamic property to get the updated prefix.
+            DynamicPartitionProperty updatedProp = 
table.getTableProperty().getDynamicPartitionProperty();
+            Assert.assertEquals("Prefix should now be 'q'", "q", 
updatedProp.getPrefix());
+
+            // Compute currentUtcBorder from the scheduler's logic.
+            RangePartitionInfo info = (RangePartitionInfo) 
table.getPartitionInfo();
+            String partitionFormat = DynamicPartitionUtil.getPartitionFormat(
+                    info.getPartitionColumns().get(0));
+            String currentUtcBorder = 
DynamicPartitionUtil.getPartitionRangeString(
+                    updatedProp, ZonedDateTime.now(ZoneOffset.UTC), 0, 
partitionFormat);
+
+            // nowPartitionName uses the new prefix "q" — no existing partition
+            // matches it, so name-based comparison would not exclude anything.
+            String newPrefixNowName = "q_nonexistent";
+
+            // 1. Without currentUtcBorder: name-based fails to exclude the
+            //    current partition because the name "q_nonexistent" matches 
nothing.
+            List<Partition> historicalNoUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                    table, newPrefixNowName, null);
+            boolean currentFoundByName = false;
+            for (Partition p : historicalNoUtc) {
+                RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+                String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+                if (lowerBound.contains(currentUtcBorder.replace("+00:00", 
""))) {
+                    currentFoundByName = true;
+                    break;
+                }
+            }
+            Assert.assertTrue("Old prefix: name-based should fail to exclude 
current partition",
+                    currentFoundByName);
+
+            // 2. With currentUtcBorder: range-based correctly excludes the
+            //    current partition despite the name mismatch.
+            List<Partition> historicalWithUtc = 
DynamicPartitionScheduler.getHistoricalPartitions(
+                    table, newPrefixNowName, currentUtcBorder);
+            boolean currentFoundByRange = false;
+            for (Partition p : historicalWithUtc) {
+                RangePartitionItem item = (RangePartitionItem) 
info.getItem(p.getId());
+                String lowerBound = 
item.getItems().lowerEndpoint().getKeys().get(0).getStringValue();
+                if (lowerBound.contains(currentUtcBorder.replace("+00:00", 
""))) {
+                    currentFoundByRange = true;
+                    break;
+                }
+            }
+            Assert.assertFalse("Old prefix: range-based must exclude current 
partition",
+                    currentFoundByRange);
+            Assert.assertEquals("Old prefix: exclude exactly one partition",
+                    totalPartitions - 1, historicalWithUtc.size());
+        } finally {
+            connectContext.getSessionVariable().setTimeZone(originalTimeZone);
+        }
+    }
+
+    @Test
+    public void testTimeStampTzGetHistoricalPartitionsNoncanonicalRange() 
throws Exception {
+        // A pre-fix partition created under Asia/Shanghai has a lower bound
+        // at 16:00Z (Shanghai midnight = UTC 16:00 previous day).  The new
+        // scheduler captures currentUtcBorder at UTC midnight (00:00Z) which
+        // falls INSIDE such a partition but does NOT equal its lower endpoint.
+        // Exact lower-bound equality would miss this partition, leaving it
+        // in the historical list and corrupting auto-bucket calculations.
+        // Range containment (lower <= rangeKey < upper) correctly excludes it.
+        String originalTimeZone = 
connectContext.getSessionVariable().getTimeZone();
+        try {
+            connectContext.getSessionVariable().setTimeZone("America/Chicago");
+
+            // Create table without dynamic partition — we add ranges manually.
+            String createSql = "CREATE TABLE test.`tstz_noncanonical` (\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"
+                    + ");";
+            createTable(createSql);
+
+            Database db = 
Env.getCurrentInternalCatalog().getDbOrAnalysisException("test");
+            OlapTable table = (OlapTable) 
db.getTableOrAnalysisException("tstz_noncanonical");
+
+            DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd 
HH:mm:ss");
+            ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
+
+            // Non-canonical partition: lower = yesterday 16:00Z, upper = 
today 16:00Z.
+            // currentUtcBorder (today 00:00Z) is inside [yesterday 16:00Z, 
today 16:00Z)
+            // but does NOT equal the lower bound (16:00Z).
+            ZonedDateTime yesterday16Z = 
now.withHour(16).withMinute(0).withSecond(0).withNano(0);

Review Comment:
   [P2] Keep this noncanonical fixture valid after 16:00 UTC
   
   When the test runs at or after 16:00Z, `now.withHour(16)` is today's 16:00 
and this branch does not subtract a day, so the constructed range is `[today 
16:00Z, tomorrow 16:00Z)`. The `currentUtcBorder` below is still today's 
00:00Z, outside that range, and the sanity assertion fails for eight hours 
every day before `getHistoricalPartitions()` is exercised. This is a separate 
test-fixture failure from the existing production discussion at line 263. 
Please derive both endpoints from a fixed clock, or derive the lower endpoint 
from today's UTC midnight minus eight hours.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to