4545import org .apache .iceberg .DeleteFiles ;
4646import org .apache .iceberg .FileContent ;
4747import org .apache .iceberg .FileScanTask ;
48+ import org .apache .iceberg .PartitionField ;
49+ import org .apache .iceberg .PartitionSpec ;
4850import org .apache .iceberg .ReachableFileUtil ;
4951import org .apache .iceberg .RewriteFiles ;
5052import org .apache .iceberg .Schema ;
6365import org .apache .iceberg .types .Conversions ;
6466import org .apache .iceberg .types .Type ;
6567import org .apache .iceberg .types .Types ;
68+ import org .apache .iceberg .util .SerializableFunction ;
6669import org .apache .iceberg .util .ThreadPools ;
6770import org .slf4j .Logger ;
6871import org .slf4j .LoggerFactory ;
7578import java .time .ZoneId ;
7679import java .time .ZoneOffset ;
7780import java .time .format .DateTimeFormatter ;
81+ import java .util .ArrayList ;
7882import java .util .Collections ;
7983import java .util .HashSet ;
84+ import java .util .List ;
8085import java .util .Locale ;
8186import java .util .Map ;
8287import java .util .Optional ;
@@ -650,7 +655,10 @@ private Set<String> deleteInvalidMetadataFile(
650655 }
651656
652657 CloseableIterable <FileEntry > fileScan (
653- Table table , Expression dataFilter , DataExpirationConfig expirationConfig ) {
658+ Table table ,
659+ Expression dataFilter ,
660+ DataExpirationConfig expirationConfig ,
661+ long expireTimestamp ) {
654662 TableScan tableScan = table .newScan ().filter (dataFilter ).includeColumnStats ();
655663
656664 CloseableIterable <FileScanTask > tasks ;
@@ -680,6 +688,7 @@ CloseableIterable<FileEntry> fileScan(
680688 .collect (Collectors .toSet ());
681689
682690 Types .NestedField field = table .schema ().findField (expirationConfig .getExpirationField ());
691+ Comparable <?> expireValue = getExpireValue (expirationConfig , field , expireTimestamp );
683692 return CloseableIterable .transform (
684693 CloseableIterable .withNoopClose (Iterables .concat (dataFiles , deleteFiles )),
685694 contentFile -> {
@@ -689,7 +698,8 @@ CloseableIterable<FileEntry> fileScan(
689698 field ,
690699 DateTimeFormatter .ofPattern (
691700 expirationConfig .getDateTimePattern (), Locale .getDefault ()),
692- expirationConfig .getNumberDateFormat ());
701+ expirationConfig .getNumberDateFormat (),
702+ expireValue );
693703 return new FileEntry (contentFile .copyWithoutStats (), literal );
694704 });
695705 }
@@ -698,7 +708,8 @@ protected ExpireFiles expiredFileScan(
698708 DataExpirationConfig expirationConfig , Expression dataFilter , long expireTimestamp ) {
699709 Map <StructLike , DataFileFreshness > partitionFreshness = Maps .newConcurrentMap ();
700710 ExpireFiles expiredFiles = new ExpireFiles ();
701- try (CloseableIterable <FileEntry > entries = fileScan (table , dataFilter , expirationConfig )) {
711+ try (CloseableIterable <FileEntry > entries =
712+ fileScan (table , dataFilter , expirationConfig , expireTimestamp )) {
702713 Queue <FileEntry > fileEntries = new LinkedTransferQueue <>();
703714 entries .forEach (
704715 e -> {
@@ -716,6 +727,33 @@ protected ExpireFiles expiredFileScan(
716727 return expiredFiles ;
717728 }
718729
730+ private Comparable <?> getExpireValue (
731+ DataExpirationConfig expirationConfig , Types .NestedField field , long expireTimestamp ) {
732+ switch (field .type ().typeId ()) {
733+ // expireTimestamp is in milliseconds, TIMESTAMP type is in microseconds
734+ case TIMESTAMP :
735+ return expireTimestamp * 1000 ;
736+ case LONG :
737+ if (expirationConfig .getNumberDateFormat ().equals (EXPIRE_TIMESTAMP_MS )) {
738+ return expireTimestamp ;
739+ } else if (expirationConfig .getNumberDateFormat ().equals (EXPIRE_TIMESTAMP_S )) {
740+ return expireTimestamp / 1000 ;
741+ } else {
742+ throw new IllegalArgumentException (
743+ "Number dateformat: " + expirationConfig .getNumberDateFormat ());
744+ }
745+ case STRING :
746+ return LocalDateTime .ofInstant (
747+ Instant .ofEpochMilli (expireTimestamp ), getDefaultZoneId (field ))
748+ .format (
749+ DateTimeFormatter .ofPattern (
750+ expirationConfig .getDateTimePattern (), Locale .getDefault ()));
751+ default :
752+ throw new IllegalArgumentException (
753+ "Unsupported expiration field type: " + field .type ().typeId ());
754+ }
755+ }
756+
719757 /**
720758 * Create a filter expression for expired files for the `FILE` level. For the `PARTITION` level,
721759 * we need to collect the oldest files to determine if the partition is obsolete, so we will not
@@ -917,17 +955,20 @@ static boolean willNotRetain(
917955 }
918956 }
919957
920- private static Literal <Long > getExpireTimestampLiteral (
958+ private Literal <Long > getExpireTimestampLiteral (
921959 ContentFile <?> contentFile ,
922960 Types .NestedField field ,
923961 DateTimeFormatter formatter ,
924- String numberDateFormatter ) {
962+ String numberDateFormatter ,
963+ Comparable <?> expireValue ) {
925964 Type type = field .type ();
926965 Object upperBound =
927966 Conversions .fromByteBuffer (type , contentFile .upperBounds ().get (field .fieldId ()));
928967 Literal <Long > literal = Literal .of (Long .MAX_VALUE );
929968 if (null == upperBound ) {
930- return literal ;
969+ if (canBeExpireByPartitionValue (contentFile , field , expireValue )) {
970+ literal = Literal .of (0L );
971+ }
931972 } else if (upperBound instanceof Long ) {
932973 switch (type .typeId ()) {
933974 case TIMESTAMP :
@@ -951,9 +992,40 @@ private static Literal<Long> getExpireTimestampLiteral(
951992 .toInstant ()
952993 .toEpochMilli ());
953994 }
995+
954996 return literal ;
955997 }
956998
999+ @ SuppressWarnings ("unchecked" )
1000+ private boolean canBeExpireByPartitionValue (
1001+ ContentFile <?> contentFile , Types .NestedField expireField , Comparable <?> expireValue ) {
1002+ PartitionSpec partitionSpec = table .specs ().get (contentFile .specId ());
1003+ int pos = 0 ;
1004+ List <Boolean > compareResults = new ArrayList <>();
1005+ for (PartitionField partitionField : partitionSpec .fields ()) {
1006+ if (partitionField .sourceId () == expireField .fieldId ()) {
1007+ if (partitionField .transform ().isVoid ()) {
1008+ return false ;
1009+ }
1010+
1011+ Comparable <?> partitionUpperBound =
1012+ ((SerializableFunction <Comparable <?>, Comparable <?>>)
1013+ partitionField .transform ().bind (expireField .type ()))
1014+ .apply (expireValue );
1015+ Comparable <Object > filePartitionValue =
1016+ contentFile .partition ().get (pos , partitionUpperBound .getClass ());
1017+ int compared = filePartitionValue .compareTo (partitionUpperBound );
1018+ Boolean compareResult =
1019+ expireField .type () == Types .StringType .get () ? compared <= 0 : compared < 0 ;
1020+ compareResults .add (compareResult );
1021+ }
1022+
1023+ pos ++;
1024+ }
1025+
1026+ return !compareResults .isEmpty () && compareResults .stream ().allMatch (Boolean ::booleanValue );
1027+ }
1028+
9571029 public Table getTable () {
9581030 return table ;
9591031 }
0 commit comments