From 42208bf4e7ec38a6f0c35a15bb0b173a9a6e9177 Mon Sep 17 00:00:00 2001 From: Eunbin Son Date: Thu, 29 Oct 2026 20:25:17 +0900 Subject: [PATCH] [core] Validate DATE partitions by their date value in partition.timestamp-format.strict PartitionTimestampValidator passed DATE partition values to PartitionTimeExtractor as raw epoch-day integers, so the documented 'yyyy-MM-dd' + '$dt' setup failed on every write once partition.timestamp-format.strict was enabled. Convert DATE values to LocalDate first, as PartitionExpireStrategy#convertPartition does. Generated-by: Claude Code --- .../operation/AbstractFileStoreWrite.java | 10 ++++ .../paimon/table/sink/TableWriteTest.java | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java index 0c0738c6173e..ba9b4223e91f 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/AbstractFileStoreWrite.java @@ -40,6 +40,7 @@ import org.apache.paimon.partition.PartitionTimeExtractor; import org.apache.paimon.table.sink.CommitMessage; import org.apache.paimon.table.sink.CommitMessageImpl; +import org.apache.paimon.types.DataTypeRoot; import org.apache.paimon.types.RowType; import org.apache.paimon.utils.CommitIncrement; import org.apache.paimon.utils.ExecutorThreadFactory; @@ -53,6 +54,7 @@ import javax.annotation.Nullable; +import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; @@ -820,6 +822,14 @@ private static PartitionTimestampValidator create( private void validate(BinaryRow partition) { Object[] array = partitionConverter.convert(partition); + RowType partitionType = partitionConverter.rowType(); + for (int i = 0; i < array.length; i++) { + // DATE is stored as an epoch day, same as PartitionExpireStrategy#convertPartition. + if (array[i] != null + && partitionType.getTypeAt(i).getTypeRoot() == DataTypeRoot.DATE) { + array[i] = LocalDate.ofEpochDay((Integer) array[i]); + } + } try { timeExtractor.extract(partitionKeys, Arrays.asList(array)); } catch (DateTimeParseException e) { diff --git a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java index 3441dadecf89..aa4f0e10c266 100644 --- a/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/table/sink/TableWriteTest.java @@ -52,6 +52,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -317,6 +318,51 @@ public void testPartitionTimestampValidation() throws Exception { write.close(); } + @Test + public void testPartitionTimestampValidationWithDatePartition() throws Exception { + RowType rowType = + RowType.of( + new DataType[] { + DataTypes.DATE().notNull(), + DataTypes.INT().notNull(), + DataTypes.BIGINT() + }, + new String[] {"dt", "k", "v"}); + int dt = (int) LocalDate.of(2026, 10, 29).toEpochDay(); + + // valid partition value: 2026-10-29 matches yyyy-MM-dd + Options conf = new Options(); + conf.set(CoreOptions.PARTITION_TIMESTAMP_FORMATTER, "yyyy-MM-dd"); + conf.set(CoreOptions.PARTITION_TIMESTAMP_PATTERN, "$dt"); + conf.set(CoreOptions.PARTITION_TIMESTAMP_FORMAT_STRICT, true); + FileStoreTable table = + createFileStoreTable( + rowType, Collections.singletonList("dt"), Arrays.asList("dt", "k"), conf); + TableWriteImpl write = table.newWrite(commitUser); + StreamTableCommit commit = table.newCommit(commitUser); + write.write(GenericRow.of(dt, 1, 1L)); + commit.commit(0, write.prepareCommit(false, 0)); + assertThat(readRows(table)).hasSize(1); + write.close(); + commit.close(); + + // invalid partition value: 2026-10-29 does not match yyyyMMdd + Options mismatchConf = new Options(); + mismatchConf.set(CoreOptions.PARTITION_TIMESTAMP_FORMATTER, "yyyyMMdd"); + mismatchConf.set(CoreOptions.PARTITION_TIMESTAMP_FORMAT_STRICT, true); + FileStoreTable mismatchTable = + createFileStoreTable( + rowType, + Collections.singletonList("dt"), + Arrays.asList("dt", "k"), + mismatchConf); + TableWriteImpl mismatchWrite = mismatchTable.newWrite(commitUser); + assertThatThrownBy(() -> mismatchWrite.write(GenericRow.of(dt, 1, 1L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("dt=2026-10-29"); + mismatchWrite.close(); + } + @Test public void testDeduplicateDeleteWithPrimaryKeyOnly() throws Exception { FileStoreTable table = createNotNullFileStoreTable(new Options());