This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 4dcd6c4e27 [core] Fix expiration of DATE partitions (#10464)
4dcd6c4e27 is described below

commit 4dcd6c4e27775d44db2bacad2915f1aeaf0e5b03
Author: jianguotian <[email protected]>
AuthorDate: Mon Oct 12 21:26:40 2026 +0800

    [core] Fix expiration of DATE partitions (#10464)
---
 docs/docs/maintenance/manage-partitions.md         |   5 +
 .../paimon/operation/NormalPartitionExpire.java    |  13 ++
 .../paimon/partition/PartitionExpireStrategy.java  |  12 +-
 .../paimon/operation/PartitionExpireTest.java      | 194 ++++++++++++++++++++-
 4 files changed, 221 insertions(+), 3 deletions(-)

diff --git a/docs/docs/maintenance/manage-partitions.md 
b/docs/docs/maintenance/manage-partitions.md
index 50870c1e9a..ab2bfc7922 100644
--- a/docs/docs/maintenance/manage-partitions.md
+++ b/docs/docs/maintenance/manage-partitions.md
@@ -80,6 +80,11 @@ CREATE TABLE t (...) PARTITIONED BY (other_key, dt) WITH (
 );
 ```
 
+For `DATE` partition columns, `values-time` uses the date value in 
`yyyy-MM-dd` format,
+not its internal epoch-day integer. Use the default timestamp formatter or a 
formatter
+that matches this representation, such as `yyyy-MM-dd` with the pattern `$dt`.
+Expiration is based on the partition value, not the partition creation time.
+
 `update-time` strategy.
 ```sql
 CREATE TABLE t (...) PARTITIONED BY (dt) WITH (
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/NormalPartitionExpire.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/NormalPartitionExpire.java
index 29aa8e0437..aaceb6a17a 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/NormalPartitionExpire.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/NormalPartitionExpire.java
@@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory;
 import javax.annotation.Nullable;
 
 import java.time.Duration;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -160,6 +161,18 @@ public class NormalPartitionExpire implements 
PartitionExpire {
         List<List<String>> expiredPartValues = new 
ArrayList<>(partitionEntries.size());
         for (PartitionEntry partition : partitionEntries) {
             Object[] array = strategy.convertPartition(partition.partition());
+            // DATE values are normalized for time extraction; use the stored 
names for dropping.
+            Map<String, String> partitionSpec =
+                    commit.pathFactory()
+                            .partitionComputer()
+                            .generatePartValues(partition.partition());
+            int index = 0;
+            for (String value : partitionSpec.values()) {
+                if (array[index] instanceof LocalDate) {
+                    array[index] = value;
+                }
+                index++;
+            }
             expiredPartValues.add(strategy.toPartitionValue(array));
         }
 
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategy.java
 
b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategy.java
index 4371fe86fa..26ca00f2f7 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategy.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategy.java
@@ -24,12 +24,14 @@ import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.manifest.PartitionEntry;
 import org.apache.paimon.operation.FileStoreScan;
+import org.apache.paimon.types.DataTypeRoot;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.RowDataToObjectArrayConverter;
 
 import javax.annotation.Nullable;
 
 import java.io.Serializable;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -73,7 +75,15 @@ public abstract class PartitionExpireStrategy implements 
Serializable {
     }
 
     public Object[] convertPartition(BinaryRow partition) {
-        return toObjectArrayConverter.convert(partition);
+        Object[] values = toObjectArrayConverter.convert(partition);
+        RowType partitionType = toObjectArrayConverter.rowType();
+        for (int index = 0; index < values.length; index++) {
+            if (values[index] != null
+                    && partitionType.getTypeAt(index).getTypeRoot() == 
DataTypeRoot.DATE) {
+                values[index] = LocalDate.ofEpochDay((Integer) values[index]);
+            }
+        }
+        return values;
     }
 
     public abstract List<PartitionEntry> selectExpiredPartitions(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/operation/PartitionExpireTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/operation/PartitionExpireTest.java
index c73c155db7..57dabaea4c 100644
--- 
a/paimon-core/src/test/java/org/apache/paimon/operation/PartitionExpireTest.java
+++ 
b/paimon-core/src/test/java/org/apache/paimon/operation/PartitionExpireTest.java
@@ -21,8 +21,10 @@ package org.apache.paimon.operation;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.io.CompactIncrement;
@@ -30,6 +32,7 @@ import org.apache.paimon.io.DataFileMeta;
 import org.apache.paimon.io.DataIncrement;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.partition.PartitionStatistics;
+import org.apache.paimon.partition.PartitionValuesTimeExpireStrategy;
 import org.apache.paimon.partition.actions.AddDonePartitionAction;
 import org.apache.paimon.schema.FileSystemSchemaManager;
 import org.apache.paimon.schema.Schema;
@@ -45,6 +48,8 @@ import org.apache.paimon.table.sink.CommitMessageImpl;
 import org.apache.paimon.table.sink.StreamTableCommit;
 import org.apache.paimon.table.sink.StreamTableWrite;
 import org.apache.paimon.table.sink.TableCommitImpl;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VarCharType;
 import org.apache.paimon.utils.SnapshotManager;
@@ -55,6 +60,8 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import java.io.IOException;
 import java.time.LocalDate;
@@ -126,7 +133,7 @@ public class PartitionExpireTest {
                             throws Catalog.TableNotExistException {
                         for (Map<String, String> partition : partitions) {
                             // only record partitions that were created
-                            if (createdPartitions.contains(partition)) {
+                            if (createdPartitions.remove(partition)) {
                                 deletedPartitions.add(partition);
                             }
                         }
@@ -229,6 +236,185 @@ public class PartitionExpireTest {
         assertThat(overwriteSnapshotCnt).isEqualTo(3L);
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"1969-12-31", "1970-01-01", "2024-02-29", 
"2026-03-11"})
+    public void testDatePartitionValues(String dateValue) {
+        LocalDate partitionDate = LocalDate.parse(dateValue);
+        RowType partitionType = RowType.of(new DataType[] {DataTypes.DATE()}, 
new String[] {"dt"});
+        BinaryRow partition = BinaryRow.singleColumn((int) 
partitionDate.toEpochDay());
+        for (String formatter : Arrays.asList(null, "yyyy-MM-dd")) {
+            for (String pattern : Arrays.asList(null, "$dt")) {
+                Options options = new Options();
+                if (formatter != null) {
+                    options.set(PARTITION_TIMESTAMP_FORMATTER, formatter);
+                }
+                if (pattern != null) {
+                    options.set(CoreOptions.PARTITION_TIMESTAMP_PATTERN, 
pattern);
+                }
+                PartitionValuesTimeExpireStrategy strategy =
+                        new PartitionValuesTimeExpireStrategy(
+                                new CoreOptions(options), partitionType);
+                Object[] values = strategy.convertPartition(partition);
+                
assertThat(strategy.toPartitionValue(values)).containsExactly(dateValue);
+                assertThat(strategy.toPartitionString(values))
+                        .containsExactly(entry("dt", dateValue));
+                
assertThat(strategy.isExpired(partitionDate.plusDays(1).atStartOfDay(), 
partition))
+                        .isTrue();
+                assertThat(strategy.isExpired(partitionDate.atStartOfDay(), 
partition)).isFalse();
+            }
+        }
+    }
+
+    @Test
+    public void testNullDatePartitionValues() {
+        RowType partitionType = RowType.of(DataTypes.DATE());
+        BinaryRow partition =
+                new 
InternalRowSerializer(partitionType).toBinaryRow(GenericRow.of((Object) null));
+        CoreOptions options = new CoreOptions(new Options());
+        PartitionValuesTimeExpireStrategy strategy =
+                new PartitionValuesTimeExpireStrategy(options, partitionType);
+        
assertThat(strategy.toPartitionValue(strategy.convertPartition(partition)))
+                .containsExactly(options.partitionDefaultName());
+        assertThat(strategy.isExpired(date(6), partition)).isFalse();
+    }
+
+    @Test
+    public void testIntegerPartitionValues() {
+        Options options = new Options();
+        options.set(PARTITION_TIMESTAMP_FORMATTER, "yyyyMMdd");
+        PartitionValuesTimeExpireStrategy strategy =
+                new PartitionValuesTimeExpireStrategy(
+                        new CoreOptions(options), RowType.of(DataTypes.INT()));
+        BinaryRow partition = BinaryRow.singleColumn(20230101);
+        
assertThat(strategy.convertPartition(partition)).containsExactly(20230101);
+        
assertThat(strategy.toPartitionValue(strategy.convertPartition(partition)))
+                .containsExactly("20230101");
+        assertThat(strategy.isExpired(date(6), partition)).isTrue();
+    }
+
+    @Test
+    public void testDateAndHourPartitionValues() {
+        RowType partitionType =
+                RowType.of(
+                        new DataType[] {DataTypes.DATE(), DataTypes.INT()},
+                        new String[] {"dt", "hour"});
+        LocalDate partitionDate = LocalDate.of(2023, 1, 1);
+        BinaryRow partition =
+                new InternalRowSerializer(partitionType)
+                        .toBinaryRow(GenericRow.of((int) 
partitionDate.toEpochDay(), 12));
+        Options options = new Options();
+        options.set(CoreOptions.PARTITION_TIMESTAMP_PATTERN, "$dt 
$hour:00:00");
+        options.set(PARTITION_TIMESTAMP_FORMATTER, "yyyy-MM-dd HH:mm:ss");
+        PartitionValuesTimeExpireStrategy strategy =
+                new PartitionValuesTimeExpireStrategy(new 
CoreOptions(options), partitionType);
+        
assertThat(strategy.toPartitionValue(strategy.convertPartition(partition)))
+                .containsExactly("2023-01-01", "12");
+        assertThat(strategy.isExpired(partitionDate.atTime(12, 0), 
partition)).isFalse();
+        assertThat(strategy.isExpired(partitionDate.atTime(13, 0), 
partition)).isTrue();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"values-time", "update-time"})
+    public void testExpireDatePartitionWithoutMetastore(String 
expirationStrategy)
+            throws Exception {
+        SchemaManager schemaManager = new 
FileSystemSchemaManager(LocalFileIO.create(), path);
+        Map<String, String> tableOptions = new HashMap<>();
+        tableOptions.put(METASTORE_PARTITIONED_TABLE.key(), "false");
+        tableOptions.put(CoreOptions.PARTITION_GENERATE_LEGACY_NAME.key(), 
"false");
+        schemaManager.createTable(
+                new Schema(
+                        RowType.of(DataTypes.DATE(), 
VarCharType.STRING_TYPE).getFields(),
+                        singletonList("f0"),
+                        emptyList(),
+                        tableOptions,
+                        ""));
+        newTable();
+        int expiredDay = (int) date(1).toLocalDate().toEpochDay();
+        int retainedDay = (int) date(5).toLocalDate().toEpochDay();
+        write(GenericRow.of(expiredDay, BinaryString.fromString("expired")));
+        write(GenericRow.of(retainedDay, BinaryString.fromString("retained")));
+
+        Map<String, String> options = new HashMap<>();
+        options.put(PARTITION_EXPIRATION_TIME.key(), "2 d");
+        options.put(CoreOptions.PARTITION_EXPIRATION_STRATEGY.key(), 
expirationStrategy);
+        options.put(PARTITION_TIMESTAMP_FORMATTER.key(), "yyyy-MM-dd");
+        options.put(CoreOptions.PARTITION_TIMESTAMP_PATTERN.key(), "$f0");
+        table = table.copy(options);
+        NormalPartitionExpire expire =
+                (NormalPartitionExpire) table.store().newPartitionExpire("", 
table);
+        expire.setLastCheck(date(1));
+        List<Map<String, String>> expectedExpired = new ArrayList<>();
+        expectedExpired.add(Collections.singletonMap("f0", "2023-01-01"));
+        LocalDateTime checkTime = date(6);
+        List<Integer> expectedRemaining = singletonList(retainedDay);
+        if ("update-time".equals(expirationStrategy)) {
+            checkTime = LocalDateTime.now().plusDays(3);
+            expectedExpired.add(Collections.singletonMap("f0", "2023-01-05"));
+            expectedRemaining = emptyList();
+        }
+        assertThat(expire.expire(checkTime, Long.MAX_VALUE))
+                .containsExactlyElementsOf(expectedExpired);
+        List<Integer> remainingDates = new ArrayList<>();
+        table.newRead()
+                .createReader(table.newScan().plan().splits())
+                .forEachRemaining(row -> remainingDates.add(row.getInt(0)));
+        
assertThat(remainingDates).containsExactlyElementsOf(expectedRemaining);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"update-time", "values-time"})
+    public void testExpireDefaultLegacyDateAndStringPartitionMetadata(String 
expirationStrategy)
+            throws Exception {
+        SchemaManager schemaManager = new 
FileSystemSchemaManager(LocalFileIO.create(), path);
+        schemaManager.createTable(
+                new Schema(
+                        RowType.of(
+                                        DataTypes.DATE(),
+                                        VarCharType.STRING_TYPE,
+                                        VarCharType.STRING_TYPE)
+                                .getFields(),
+                        Arrays.asList("f0", "f1"),
+                        emptyList(),
+                        
Collections.singletonMap(METASTORE_PARTITIONED_TABLE.key(), "true"),
+                        ""));
+        newTable();
+        assertThat(table.coreOptions().legacyPartitionName()).isTrue();
+        int epochDay = (int) date(1).toLocalDate().toEpochDay();
+        write(
+                GenericRow.of(
+                        epochDay,
+                        BinaryString.fromString("west"),
+                        BinaryString.fromString("expired")));
+        Map<String, String> expectedPartition = new LinkedHashMap<>();
+        expectedPartition.put("f0", Integer.toString(epochDay));
+        expectedPartition.put("f1", "west");
+        assertThat(createdPartitions).containsExactly(expectedPartition);
+
+        Map<String, String> options = new HashMap<>();
+        options.put(PARTITION_EXPIRATION_TIME.key(), "2 d");
+        options.put(CoreOptions.PARTITION_EXPIRATION_STRATEGY.key(), 
expirationStrategy);
+        options.put(PARTITION_TIMESTAMP_FORMATTER.key(), "yyyy-MM-dd");
+        options.put(CoreOptions.PARTITION_TIMESTAMP_PATTERN.key(), "$f0");
+        table = table.copy(options);
+        NormalPartitionExpire expire =
+                (NormalPartitionExpire) table.store().newPartitionExpire("", 
table);
+        expire.setLastCheck(date(1));
+        LocalDateTime checkTime =
+                "update-time".equals(expirationStrategy)
+                        ? LocalDateTime.now().plusDays(3)
+                        : date(6);
+        List<Map<String, String>> expired = expire.expire(checkTime, 
Long.MAX_VALUE);
+
+        List<String> remaining = new ArrayList<>();
+        table.newRead()
+                .createReader(table.newScan().plan().splits())
+                .forEachRemaining(row -> 
remaining.add(row.getString(2).toString()));
+        assertThat(remaining).isEmpty();
+        assertThat(createdPartitions).isEmpty();
+        assertThat(deletedPartitions).containsExactly(expectedPartition);
+        assertThat(expired).containsExactly(expectedPartition);
+    }
+
     @Test
     public void testExpireWithNullOrEmptyPartition() throws Exception {
         SchemaManager schemaManager = new 
FileSystemSchemaManager(LocalFileIO.create(), path);
@@ -521,9 +707,13 @@ public class PartitionExpireTest {
     }
 
     private List<CommitMessage> write(String f0, String f1) throws Exception {
+        return write(GenericRow.of(BinaryString.fromString(f0), 
BinaryString.fromString(f1)));
+    }
+
+    private List<CommitMessage> write(GenericRow row) throws Exception {
         StreamTableWrite write =
                 table.copy(Collections.singletonMap(WRITE_ONLY.key(), 
"true")).newWrite("");
-        write.write(GenericRow.of(BinaryString.fromString(f0), 
BinaryString.fromString(f1)));
+        write.write(row);
         TableCommitImpl commit = table.newCommit("");
         List<CommitMessage> commitMessages = write.prepareCommit(true, 0);
         commit.commit(0, commitMessages);

Reply via email to