This is an automated email from the ASF dual-hosted git repository. lzljs3620320 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 13072be478 [core] fix NPE and ArrayIndexOutOfBoundsException for PartitionExpire (#6150) 13072be478 is described below commit 13072be47864460b572853e8fcddb29f61418e5b Author: LsomeYeah <94825748+lsomey...@users.noreply.github.com> AuthorDate: Tue Aug 26 19:27:37 2025 +0800 [core] fix NPE and ArrayIndexOutOfBoundsException for PartitionExpire (#6150) --- .../apache/paimon/operation/PartitionExpire.java | 3 ++- .../paimon/partition/PartitionExpireStrategy.java | 14 ++++++++--- .../partition/PartitionExpireStrategyFactory.java | 6 ++++- .../PartitionUpdateTimeExpireStrategy.java | 5 ++-- .../PartitionValuesTimeExpireStrategy.java | 2 +- .../paimon/operation/PartitionExpireTest.java | 29 ++++++++++++++++++++++ .../CustomPartitionExpirationFactory.java | 8 ++++-- 7 files changed, 56 insertions(+), 11 deletions(-) diff --git a/paimon-core/src/main/java/org/apache/paimon/operation/PartitionExpire.java b/paimon-core/src/main/java/org/apache/paimon/operation/PartitionExpire.java index 7819063f2e..36d6976152 100644 --- a/paimon-core/src/main/java/org/apache/paimon/operation/PartitionExpire.java +++ b/paimon-core/src/main/java/org/apache/paimon/operation/PartitionExpire.java @@ -185,7 +185,8 @@ public class PartitionExpire { return expiredPartValues.stream() .map(values -> String.join(DELIMITER, values)) .sorted() - .map(s -> s.split(DELIMITER)) + // Use split(DELIMITER, -1) to preserve trailing empty strings + .map(s -> s.split(DELIMITER, -1)) .map(strategy::toPartitionString) .limit(Math.min(expiredPartValues.size(), maxExpireNum)) .collect(Collectors.toList()); 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 7d016c0931..ce021eb1d3 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 @@ -39,11 +39,13 @@ import java.util.Map; public abstract class PartitionExpireStrategy { protected final List<String> partitionKeys; + protected final String partitionDefaultName; private final RowDataToObjectArrayConverter toObjectArrayConverter; - public PartitionExpireStrategy(RowType partitionType) { + public PartitionExpireStrategy(RowType partitionType, String partitionDefaultName) { this.toObjectArrayConverter = new RowDataToObjectArrayConverter(partitionType); this.partitionKeys = partitionType.getFieldNames(); + this.partitionDefaultName = partitionDefaultName; } public Map<String, String> toPartitionString(Object[] array) { @@ -57,7 +59,11 @@ public abstract class PartitionExpireStrategy { public List<String> toPartitionValue(Object[] array) { List<String> list = new ArrayList<>(partitionKeys.size()); for (int i = 0; i < partitionKeys.size(); i++) { - list.add(array[i].toString()); + if (array[i] != null) { + list.add(array[i].toString()); + } else { + list.add(partitionDefaultName); + } } return list; } @@ -76,13 +82,13 @@ public abstract class PartitionExpireStrategy { @Nullable Identifier identifier) { switch (options.partitionExpireStrategy()) { case UPDATE_TIME: - return new PartitionUpdateTimeExpireStrategy(partitionType); + return new PartitionUpdateTimeExpireStrategy(options, partitionType); case VALUES_TIME: return new PartitionValuesTimeExpireStrategy(options, partitionType); case CUSTOM: return PartitionExpireStrategyFactory.INSTANCE .get() - .create(catalogLoader, identifier, partitionType); + .create(catalogLoader, identifier, options, partitionType); default: throw new IllegalArgumentException( "Unknown partitionExpireStrategy: " + options.partitionExpireStrategy()); diff --git a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategyFactory.java b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategyFactory.java index 6e89e98725..d871e4cfd2 100644 --- a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategyFactory.java +++ b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionExpireStrategyFactory.java @@ -18,6 +18,7 @@ package org.apache.paimon.partition; +import org.apache.paimon.CoreOptions; import org.apache.paimon.catalog.CatalogLoader; import org.apache.paimon.catalog.Identifier; import org.apache.paimon.factories.FactoryUtil; @@ -30,7 +31,10 @@ import org.apache.paimon.shade.guava30.com.google.common.base.Suppliers; public interface PartitionExpireStrategyFactory { PartitionExpireStrategy create( - CatalogLoader catalogLoader, Identifier identifier, RowType partitionType); + CatalogLoader catalogLoader, + Identifier identifier, + CoreOptions options, + RowType partitionType); Supplier<PartitionExpireStrategyFactory> INSTANCE = Suppliers.memoize( diff --git a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionUpdateTimeExpireStrategy.java b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionUpdateTimeExpireStrategy.java index c2d75e8e74..3cb7a405d2 100644 --- a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionUpdateTimeExpireStrategy.java +++ b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionUpdateTimeExpireStrategy.java @@ -18,6 +18,7 @@ package org.apache.paimon.partition; +import org.apache.paimon.CoreOptions; import org.apache.paimon.manifest.PartitionEntry; import org.apache.paimon.operation.FileStoreScan; import org.apache.paimon.types.RowType; @@ -33,8 +34,8 @@ import java.util.stream.Collectors; */ public class PartitionUpdateTimeExpireStrategy extends PartitionExpireStrategy { - public PartitionUpdateTimeExpireStrategy(RowType partitionType) { - super(partitionType); + public PartitionUpdateTimeExpireStrategy(CoreOptions options, RowType partitionType) { + super(partitionType, options.partitionDefaultName()); } @Override diff --git a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionValuesTimeExpireStrategy.java b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionValuesTimeExpireStrategy.java index 51c53282c4..70c55cfb38 100644 --- a/paimon-core/src/main/java/org/apache/paimon/partition/PartitionValuesTimeExpireStrategy.java +++ b/paimon-core/src/main/java/org/apache/paimon/partition/PartitionValuesTimeExpireStrategy.java @@ -48,7 +48,7 @@ public class PartitionValuesTimeExpireStrategy extends PartitionExpireStrategy { private final PartitionTimeExtractor timeExtractor; public PartitionValuesTimeExpireStrategy(CoreOptions options, RowType partitionType) { - super(partitionType); + super(partitionType, options.partitionDefaultName()); String timePattern = options.partitionTimestampPattern(); String timeFormatter = options.partitionTimestampFormatter(); this.timeExtractor = new PartitionTimeExtractor(timePattern, timeFormatter); 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 2b45ad4352..4c48671add 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 @@ -61,6 +61,7 @@ import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -217,6 +218,34 @@ public class PartitionExpireTest { assertThat(overwriteSnapshotCnt).isEqualTo(3L); } + @Test + public void testExpireWithNullOrEmptyPartition() throws Exception { + SchemaManager schemaManager = new SchemaManager(LocalFileIO.create(), path); + schemaManager.createTable( + new Schema( + RowType.of(VarCharType.STRING_TYPE, VarCharType.STRING_TYPE).getFields(), + Arrays.asList("f0", "f1"), + emptyList(), + Collections.singletonMap(METASTORE_PARTITIONED_TABLE.key(), "true"), + "")); + newTable(); + write("20230101", "11"); + write("20230101", "12"); + // sub partition is null + write("20230101", null); + // sub partition is empty string + write("20230103", ""); + write("20230103", "32"); + write("20230105", "51"); + + PartitionExpire expire = newExpire(); + expire.setLastCheck(date(1)); + Assertions.assertDoesNotThrow(() -> expire.expire(date(6), Long.MAX_VALUE)); + + // null partition and empty string partition should be expired + assertThat(read()).containsExactlyInAnyOrder("20230105:51"); + } + @Test public void test() throws Exception { SchemaManager schemaManager = new SchemaManager(LocalFileIO.create(), path); diff --git a/paimon-core/src/test/java/org/apache/paimon/partition/CustomPartitionExpirationFactory.java b/paimon-core/src/test/java/org/apache/paimon/partition/CustomPartitionExpirationFactory.java index 4f2b770d55..9fa6369f93 100644 --- a/paimon-core/src/test/java/org/apache/paimon/partition/CustomPartitionExpirationFactory.java +++ b/paimon-core/src/test/java/org/apache/paimon/partition/CustomPartitionExpirationFactory.java @@ -18,6 +18,7 @@ package org.apache.paimon.partition; +import org.apache.paimon.CoreOptions; import org.apache.paimon.catalog.Catalog; import org.apache.paimon.catalog.CatalogLoader; import org.apache.paimon.catalog.Identifier; @@ -38,8 +39,11 @@ public class CustomPartitionExpirationFactory implements PartitionExpireStrategy @Override public PartitionExpireStrategy create( - CatalogLoader catalogLoader, Identifier identifier, RowType partitionType) { - return new PartitionExpireStrategy(partitionType) { + CatalogLoader catalogLoader, + Identifier identifier, + CoreOptions options, + RowType partitionType) { + return new PartitionExpireStrategy(partitionType, options.partitionDefaultName()) { @Override public List<PartitionEntry> selectExpiredPartitions( FileStoreScan scan, LocalDateTime expirationTime) {