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