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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6ab54c515 [AMORO-4316][AMS] Support monthly period for auto-creating 
Iceberg tags (#4317)
6ab54c515 is described below

commit 6ab54c515f9eb698812b95589fed60e2325d65ad
Author: seoyeon <[email protected]>
AuthorDate: Tue Aug 18 15:14:57 2026 +0900

    [AMORO-4316][AMS] Support monthly period for auto-creating Iceberg tags 
(#4317)
    
    [AMORO-4316] add monthly auto-create tag
    
    Co-authored-by: seoyoniee <[email protected]>
    Co-authored-by: ZhouJinsong <[email protected]>
---
 .../amoro/server/table/TableConfigurations.java    |  3 +
 .../org/apache/amoro/config/TagConfiguration.java  | 18 ++++-
 .../org/apache/amoro/table/TableProperties.java    |  1 +
 .../maintainer/TestAutoCreateIcebergTagAction.java | 88 ++++++++++++++++++++++
 docs/user-guides/configurations.md                 | 16 ++--
 5 files changed, 117 insertions(+), 9 deletions(-)

diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/table/TableConfigurations.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/table/TableConfigurations.java
index 2c403ebf3..c88275ecc 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/table/TableConfigurations.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/table/TableConfigurations.java
@@ -389,6 +389,9 @@ public class TableConfigurations {
       case HOURLY:
         defaultFormat = TableProperties.AUTO_CREATE_TAG_FORMAT_HOURLY_DEFAULT;
         break;
+      case MONTHLY:
+        defaultFormat = TableProperties.AUTO_CREATE_TAG_FORMAT_MONTHLY_DEFAULT;
+        break;
       default:
         throw new IllegalArgumentException(
             "Unsupported trigger period: " + tagConfig.getTriggerPeriod());
diff --git 
a/amoro-common/src/main/java/org/apache/amoro/config/TagConfiguration.java 
b/amoro-common/src/main/java/org/apache/amoro/config/TagConfiguration.java
index 43c1bdbdb..63106a110 100644
--- a/amoro-common/src/main/java/org/apache/amoro/config/TagConfiguration.java
+++ b/amoro-common/src/main/java/org/apache/amoro/config/TagConfiguration.java
@@ -26,6 +26,7 @@ import java.time.Duration;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAmount;
 
 /** Configuration for auto creating tags. */
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -67,6 +68,21 @@ public class TagConfiguration {
       public LocalDateTime getTagTime(LocalDateTime checkTime, int 
triggerOffsetMinutes) {
         return 
checkTime.minusMinutes(triggerOffsetMinutes).truncatedTo(ChronoUnit.HOURS);
       }
+    },
+
+    MONTHLY("monthly") {
+      @Override
+      protected TemporalAmount periodDuration() {
+        return java.time.Period.ofMonths(1);
+      }
+
+      @Override
+      public LocalDateTime getTagTime(LocalDateTime checkTime, int 
triggerOffsetMinutes) {
+        return checkTime
+            .minusMinutes(triggerOffsetMinutes)
+            .withDayOfMonth(1)
+            .truncatedTo(ChronoUnit.DAYS);
+      }
     };
 
     private final String propertyName;
@@ -79,7 +95,7 @@ public class TagConfiguration {
       return propertyName;
     }
 
-    protected abstract Duration periodDuration();
+    protected abstract TemporalAmount periodDuration();
 
     /**
      * Obtain the tag time for creating a tag, which is the ideal time of the 
last tag before the
diff --git 
a/amoro-format-iceberg/src/main/java/org/apache/amoro/table/TableProperties.java
 
b/amoro-format-iceberg/src/main/java/org/apache/amoro/table/TableProperties.java
index 6c17f6ad0..daebf2adb 100644
--- 
a/amoro-format-iceberg/src/main/java/org/apache/amoro/table/TableProperties.java
+++ 
b/amoro-format-iceberg/src/main/java/org/apache/amoro/table/TableProperties.java
@@ -269,6 +269,7 @@ public class TableProperties {
   public static final String AUTO_CREATE_TAG_FORMAT = 
"tag.auto-create.tag-format";
   public static final String AUTO_CREATE_TAG_FORMAT_DAILY_DEFAULT = 
"'tag-'yyyyMMdd";
   public static final String AUTO_CREATE_TAG_FORMAT_HOURLY_DEFAULT = 
"'tag-'yyyyMMddHH";
+  public static final String AUTO_CREATE_TAG_FORMAT_MONTHLY_DEFAULT = 
"'tag-'yyyyMM";
 
   /** table write related properties */
   public static final String FILE_FORMAT_PARQUET = "parquet";
diff --git 
a/amoro-format-iceberg/src/test/java/org/apache/amoro/formats/iceberg/maintainer/TestAutoCreateIcebergTagAction.java
 
b/amoro-format-iceberg/src/test/java/org/apache/amoro/formats/iceberg/maintainer/TestAutoCreateIcebergTagAction.java
index 4eb637edd..7d62769ba 100644
--- 
a/amoro-format-iceberg/src/test/java/org/apache/amoro/formats/iceberg/maintainer/TestAutoCreateIcebergTagAction.java
+++ 
b/amoro-format-iceberg/src/test/java/org/apache/amoro/formats/iceberg/maintainer/TestAutoCreateIcebergTagAction.java
@@ -110,6 +110,58 @@ public class TestAutoCreateIcebergTagAction extends 
TableTestBase {
     checkTagCount(table, 1);
   }
 
+  @Test
+  public void testCreateMonthlyTag() {
+    Table table = getMixedTable().asUnkeyedTable();
+    table
+        .updateProperties()
+        .set(TableProperties.ENABLE_AUTO_CREATE_TAG, "true")
+        .set(TableProperties.AUTO_CREATE_TAG_MAX_DELAY_MINUTES, "0")
+        .set(TableProperties.AUTO_CREATE_TAG_TRIGGER_PERIOD, "monthly")
+        .commit();
+    table.newAppend().commit();
+    checkSnapshots(table, 1);
+    checkNoTag(table);
+
+    Snapshot snapshot = table.currentSnapshot();
+    LocalDateTime now = fromEpochMillis(snapshot.timestampMillis());
+    newAutoCreateIcebergTagAction(table, now).execute();
+    checkTagCount(table, 1);
+    checkTag(table, "tag-" + formatMonth(now.minusMonths(1)), snapshot);
+
+    // should not recreate tag
+    newAutoCreateIcebergTagAction(table, now).execute();
+    checkTagCount(table, 1);
+  }
+
+  /**
+   * A calendar month is not a fixed-length duration, so the tag name must be 
derived with {@link
+   * java.time.Period}. Deriving it with a fixed {@code Duration.ofDays(30)} 
would name both the
+   * January and the February tag {@code tag-202601}, and the February tag 
would then never be
+   * created because {@code tagExist()} finds the January one.
+   */
+  @Test
+  public void testMonthlyTagNameAcrossMonthBoundary() {
+    Assert.assertEquals(
+        "tag-202601",
+        TagConfiguration.Period.MONTHLY.generateTagName(
+            LocalDateTime.parse("2026-02-01T00:00:00"), "'tag-'yyyyMM"));
+    Assert.assertEquals(
+        "tag-202602",
+        TagConfiguration.Period.MONTHLY.generateTagName(
+            LocalDateTime.parse("2026-03-01T00:00:00"), "'tag-'yyyyMM"));
+    // leap year
+    Assert.assertEquals(
+        "tag-202402",
+        TagConfiguration.Period.MONTHLY.generateTagName(
+            LocalDateTime.parse("2024-03-01T00:00:00"), "'tag-'yyyyMM"));
+    // year boundary
+    Assert.assertEquals(
+        "tag-202612",
+        TagConfiguration.Period.MONTHLY.generateTagName(
+            LocalDateTime.parse("2027-01-01T00:00:00"), "'tag-'yyyyMM"));
+  }
+
   @Test
   public void testCreateDailyOffsetTag() {
     Table table = getMixedTable().asUnkeyedTable();
@@ -274,6 +326,14 @@ public class TestAutoCreateIcebergTagAction extends 
TableTestBase {
     testTagTimePeriodDaily("2022-08-08T03:40:00", 30, "2022-08-08T00:00:00");
     testTagTimePeriodDaily("2022-08-08T23:40:00", 15, "2022-08-08T00:00:00");
     testTagTimePeriodDaily("2022-08-09T00:10:00", 30, "2022-08-08T00:00:00");
+
+    testTagTimePeriodMonthly("2022-08-08T03:40:00", 30, "2022-08-01T00:00:00");
+    testTagTimePeriodMonthly("2022-08-31T23:40:00", 15, "2022-08-01T00:00:00");
+    // the offset keeps the tag time on the previous month right after the 
month boundary
+    testTagTimePeriodMonthly("2022-08-01T00:10:00", 30, "2022-07-01T00:00:00");
+    // February has 28 days in 2022, 29 in 2024
+    testTagTimePeriodMonthly("2022-03-01T00:10:00", 30, "2022-02-01T00:00:00");
+    testTagTimePeriodMonthly("2024-03-01T00:10:00", 30, "2024-02-01T00:00:00");
   }
 
   @Test
@@ -349,6 +409,27 @@ public class TestAutoCreateIcebergTagAction extends 
TableTestBase {
     Assert.assertEquals(expectedTriggerTime, actualTriggerTime);
   }
 
+  private void testTagTimePeriodMonthly(
+      String checkTimeStr, int offsetMinutes, String expectedResultStr) {
+    LocalDateTime checkTime = LocalDateTime.parse(checkTimeStr);
+    Long expectedTriggerTime =
+        (expectedResultStr == null)
+            ? null
+            : LocalDateTime.parse(expectedResultStr)
+                .atZone(ZoneId.systemDefault())
+                .toInstant()
+                .toEpochMilli();
+
+    Long actualTriggerTime =
+        TagConfiguration.Period.MONTHLY
+            .getTagTime(checkTime, offsetMinutes)
+            .atZone(ZoneId.systemDefault())
+            .toInstant()
+            .toEpochMilli();
+
+    Assert.assertEquals(expectedTriggerTime, actualTriggerTime);
+  }
+
   /**
    * Parse tag configuration from table properties. This is a test helper 
method that replicates the
    * logic from TableConfigurations to avoid AMS dependency.
@@ -376,6 +457,9 @@ public class TestAutoCreateIcebergTagAction extends 
TableTestBase {
       case HOURLY:
         defaultFormat = TableProperties.AUTO_CREATE_TAG_FORMAT_HOURLY_DEFAULT;
         break;
+      case MONTHLY:
+        defaultFormat = TableProperties.AUTO_CREATE_TAG_FORMAT_MONTHLY_DEFAULT;
+        break;
       default:
         throw new IllegalArgumentException(
             "Unsupported trigger period: " + tagConfig.getTriggerPeriod());
@@ -425,6 +509,10 @@ public class TestAutoCreateIcebergTagAction extends 
TableTestBase {
     return localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
   }
 
+  private String formatMonth(LocalDateTime localDateTime) {
+    return localDateTime.format(DateTimeFormatter.ofPattern("yyyyMM"));
+  }
+
   private void checkNoTag(Table table) {
     
Assert.assertFalse(table.refs().values().stream().anyMatch(SnapshotRef::isTag));
   }
diff --git a/docs/user-guides/configurations.md 
b/docs/user-guides/configurations.md
index beb092022..4cfd5fd3a 100644
--- a/docs/user-guides/configurations.md
+++ b/docs/user-guides/configurations.md
@@ -93,14 +93,14 @@ Data-cleaning configurations are applicable to both Iceberg 
Format and Mixed str
 Tags configurations are applicable to Iceberg Format only now, and will be 
supported in Mixed Format
 soon.
 
-| Key                                       | Default                          
                                | Description                                   
                                                                                
       |
-|-------------------------------------------|------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
-| tag.auto-create.enabled                   | false                            
                                | Enables automatically creating tags           
                                                                                
       |
-| tag.auto-create.trigger.period            | daily                            
                                | Period of creating tags, support 
`daily`,`hourly` now                                                            
                    |
-| tag.auto-create.trigger.offset.minutes    | 0                                
                                | The minutes by which the tag is created after 
midnight (00:00)                                                                
       |
-| tag.auto-create.trigger.max-delay.minutes | 60                               
                                | The maximum delay time for creating a tag     
                                                                                
       |
-| tag.auto-create.tag-format                | 'tag-'yyyyMMdd for daily and 
'tag-'yyyyMMddHH for hourly periods | The format of the name for tag. Modifying 
this configuration will not take effect on old tags                             
           |
-| tag.auto-create.max-age-ms                | -1                               
                                | Time of automatically created Tag to retain, 
-1 means keep it forever. Modifying this configuration will not take effect on 
old tags |
+| Key                                       | Default                          
                                                          | Description         
                                                                                
                                |
+|-------------------------------------------|--------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
+| tag.auto-create.enabled                   | false                            
                                                          | Enables 
automatically creating tags                                                     
                                            |
+| tag.auto-create.trigger.period            | daily                            
                                                          | Period of creating 
tags, support `daily`,`hourly`,`monthly` now                                    
                                 |
+| tag.auto-create.trigger.offset.minutes    | 0                                
                                                          | The minutes by 
which the tag is created after midnight (00:00)                                 
                                     |
+| tag.auto-create.trigger.max-delay.minutes | 60                               
                                                          | The maximum delay 
time for creating a tag                                                         
                                  |
+| tag.auto-create.tag-format                | 'tag-'yyyyMMdd for daily, 
'tag-'yyyyMMddHH for hourly and 'tag-'yyyyMM for monthly periods | The format 
of the name for tag. Modifying this configuration will not take effect on old 
tags                                       |
+| tag.auto-create.max-age-ms                | -1                               
                                                          | Time of 
automatically created Tag to retain, -1 means keep it forever. Modifying this 
configuration will not take effect on old tags |
 
 ## Mixed Format configurations
 

Reply via email to