This is an automated email from the ASF dual-hosted git repository. roryqi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit b27fe4d9a4643895a50c041260291538ae25e6bf Author: MaSai <[email protected]> AuthorDate: Mon Oct 26 16:50:00 2026 +0800 [#14227] refactor(policy): Align PolicyExecutionSpec with module value-object conventions (#14228) Align `PolicyExecutionSpec` with existing policy-module value objects (`TagValueSelector` / content factories): - Drop `withXxx` copy-mutators; construct only via `of` / `empty` - Enforce shared peer constraints in `of` (no deferred `validate()`) - Store option maps as `ImmutableMap`; add `@Evolving` - DTO peer setters rebuild via `ExecutionPeersDTO` + `PolicyExecutionSpec.of` - Update unit tests for construction-time validation Keep the TMS execution-peer value type consistent with the rest of the `api` policy package. Fix: #14227 Yes — breaking for callers of removed `PolicyExecutionSpec.with*` / `validate()`. Wire JSON peers unchanged. ```bash ./gradlew :api:test --tests 'org.apache.gravitino.policy.TestPolicy*' \ :common:test --tests 'org.apache.gravitino.dto.policy.TestPolicyDTO' \ --tests 'org.apache.gravitino.dto.policy.TestOrphanFileRemovalPolicyDTO' \ --tests 'org.apache.gravitino.dto.policy.TestRewriteManifestsAndSnapshotExpirationPolicyDTO' \ -PskipITs ``` Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <[email protected]> --- .../policy/AbstractExecutablePolicyContent.java | 8 +- .../gravitino/policy/PolicyExecutionSpec.java | 117 ++---- .../gravitino/policy/TestPolicyContents.java | 42 +- .../gravitino/policy/TestPolicyExecutionSpec.java | 53 ++- .../gravitino/dto/policy/PolicyContentDTO.java | 460 +++++++++++++-------- 5 files changed, 397 insertions(+), 283 deletions(-) diff --git a/api/src/main/java/org/apache/gravitino/policy/AbstractExecutablePolicyContent.java b/api/src/main/java/org/apache/gravitino/policy/AbstractExecutablePolicyContent.java index c24a827699..1b6d1ccced 100644 --- a/api/src/main/java/org/apache/gravitino/policy/AbstractExecutablePolicyContent.java +++ b/api/src/main/java/org/apache/gravitino/policy/AbstractExecutablePolicyContent.java @@ -55,16 +55,16 @@ public abstract class AbstractExecutablePolicyContent implements ExecutablePolic } /** - * Validates universal {@link PolicyContent} rules plus shared execution-peer checks. + * Validates universal {@link PolicyContent} rules. * - * <p>Does not enforce type-specific maintenance contracts (for example orphan cleanup forbidding - * {@code commitTrigger}, or cron parsing). Those belong on concrete subclasses or the server + * <p>Shared execution-peer constraints are enforced when constructing {@link + * PolicyExecutionSpec}. Type-specific maintenance contracts (for example orphan cleanup + * forbidding {@code commitTrigger}, or cron parsing) belong on concrete subclasses or the server * create/alter path with the real {@link Policy.BuiltInType}. */ @Override public void validate() throws IllegalArgumentException { ExecutablePolicyContent.super.validate(); - executionSpec().validate(); } /** diff --git a/api/src/main/java/org/apache/gravitino/policy/PolicyExecutionSpec.java b/api/src/main/java/org/apache/gravitino/policy/PolicyExecutionSpec.java index 24ab7ba572..9783c65581 100644 --- a/api/src/main/java/org/apache/gravitino/policy/PolicyExecutionSpec.java +++ b/api/src/main/java/org/apache/gravitino/policy/PolicyExecutionSpec.java @@ -19,12 +19,12 @@ package org.apache.gravitino.policy; import com.google.common.base.Preconditions; -import java.util.Collections; -import java.util.LinkedHashMap; +import com.google.common.collect.ImmutableMap; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.annotation.Evolving; /** * Immutable execution peer fields for executable policy contents: triggers, cooldown, and option @@ -36,20 +36,21 @@ import org.apache.commons.lang3.StringUtils; * keys to and from this value object via custom Jackson serdes (same idea as {@code * DistributionSerializer}). * - * <p>Invoked from {@link AbstractExecutablePolicyContent}, not from universal {@link - * PolicyContent#validate()}. Type-specific rules (for example rejecting {@code commitTrigger} for - * orphan cleanup) belong on the concrete content or the server create/alter path. + * <p>Shared peer constraints are checked in {@link #of}; type-specific rules (for example rejecting + * {@code commitTrigger} for orphan cleanup) belong on the concrete content or the server + * create/alter path. * * <p>{@code timeTrigger} cron syntax is <strong>not</strong> validated here. The scheduler * registration path (db-scheduler / runtime cron parser) is the source of truth; validating with a * regex or a second library in {@code api} would drift from that runtime and pull scheduler * dependencies into the public API module. */ +@Evolving public final class PolicyExecutionSpec { /** Spec with every peer unset. */ public static final PolicyExecutionSpec EMPTY = - new PolicyExecutionSpec(null, null, null, null, null); + new PolicyExecutionSpec(null, null, null, ImmutableMap.of(), ImmutableMap.of()); @Nullable private final String timeTrigger; @Nullable private final Boolean commitTrigger; @@ -61,13 +62,13 @@ public final class PolicyExecutionSpec { @Nullable String timeTrigger, @Nullable Boolean commitTrigger, @Nullable Long minIntervalMs, - @Nullable Map<String, String> jobOptions, - @Nullable Map<String, String> updaterOptions) { - this.timeTrigger = StringUtils.isBlank(timeTrigger) ? null : timeTrigger; + Map<String, String> jobOptions, + Map<String, String> updaterOptions) { + this.timeTrigger = timeTrigger; this.commitTrigger = commitTrigger; this.minIntervalMs = minIntervalMs; - this.jobOptions = copyOptions(jobOptions); - this.updaterOptions = copyOptions(updaterOptions); + this.jobOptions = jobOptions; + this.updaterOptions = updaterOptions; } /** @@ -80,6 +81,8 @@ public final class PolicyExecutionSpec { * @param jobOptions optional job options * @param updaterOptions optional updater options * @return immutable spec, or {@link #EMPTY} when every peer is unset + * @throws IllegalArgumentException if {@code minIntervalMs} is negative or an option map has a + * blank key or value */ public static PolicyExecutionSpec of( @Nullable String timeTrigger, @@ -87,11 +90,28 @@ public final class PolicyExecutionSpec { @Nullable Long minIntervalMs, @Nullable Map<String, String> jobOptions, @Nullable Map<String, String> updaterOptions) { - if (!hasAny(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions)) { + String normalizedTimeTrigger = StringUtils.isBlank(timeTrigger) ? null : timeTrigger; + Map<String, String> normalizedJobOptions = copyOptions(jobOptions); + Map<String, String> normalizedUpdaterOptions = copyOptions(updaterOptions); + if (minIntervalMs != null) { + Preconditions.checkArgument(minIntervalMs >= 0, "minIntervalMs must be >= 0"); + } + validateOptionMap(normalizedJobOptions, "jobOptions"); + validateOptionMap(normalizedUpdaterOptions, "updaterOptions"); + if (!hasAny( + normalizedTimeTrigger, + commitTrigger, + minIntervalMs, + normalizedJobOptions, + normalizedUpdaterOptions)) { return EMPTY; } return new PolicyExecutionSpec( - timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); + normalizedTimeTrigger, + commitTrigger, + minIntervalMs, + normalizedJobOptions, + normalizedUpdaterOptions); } /** @@ -147,63 +167,6 @@ public final class PolicyExecutionSpec { return hasAny(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); } - /** - * @param timeTrigger replacement cron expression - * @return new spec with updated {@code timeTrigger} - */ - public PolicyExecutionSpec withTimeTrigger(@Nullable String timeTrigger) { - return of(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); - } - - /** - * @param commitTrigger replacement commit-trigger flag - * @return new spec with updated {@code commitTrigger} - */ - public PolicyExecutionSpec withCommitTrigger(@Nullable Boolean commitTrigger) { - return of(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); - } - - /** - * @param minIntervalMs replacement cooldown - * @return new spec with updated {@code minIntervalMs} - */ - public PolicyExecutionSpec withMinIntervalMs(@Nullable Long minIntervalMs) { - return of(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); - } - - /** - * @param jobOptions replacement job options - * @return new spec with updated {@code jobOptions} - */ - public PolicyExecutionSpec withJobOptions(@Nullable Map<String, String> jobOptions) { - return of(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); - } - - /** - * @param updaterOptions replacement updater options - * @return new spec with updated {@code updaterOptions} - */ - public PolicyExecutionSpec withUpdaterOptions(@Nullable Map<String, String> updaterOptions) { - return of(timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); - } - - /** - * Validates shared peer constraints (cooldown and option maps). - * - * <p>Does not parse {@code timeTrigger}; cron validity is checked when the expression is - * registered with the scheduler. - */ - public void validate() { - if (!hasAny()) { - return; - } - if (minIntervalMs != null) { - Preconditions.checkArgument(minIntervalMs >= 0, "minIntervalMs must be >= 0"); - } - validateOptionMap(jobOptions, "jobOptions"); - validateOptionMap(updaterOptions, "updaterOptions"); - } - @Override public boolean equals(Object other) { if (this == other) { @@ -253,24 +216,24 @@ public final class PolicyExecutionSpec { @Nullable String timeTrigger, @Nullable Boolean commitTrigger, @Nullable Long minIntervalMs, - @Nullable Map<String, String> jobOptions, - @Nullable Map<String, String> updaterOptions) { + Map<String, String> jobOptions, + Map<String, String> updaterOptions) { return StringUtils.isNotBlank(timeTrigger) || commitTrigger != null || minIntervalMs != null - || (jobOptions != null && !jobOptions.isEmpty()) - || (updaterOptions != null && !updaterOptions.isEmpty()); + || !jobOptions.isEmpty() + || !updaterOptions.isEmpty(); } private static Map<String, String> copyOptions(@Nullable Map<String, String> options) { if (options == null || options.isEmpty()) { - return Collections.emptyMap(); + return ImmutableMap.of(); } - return Collections.unmodifiableMap(new LinkedHashMap<>(options)); + return ImmutableMap.copyOf(options); } private static void validateOptionMap(Map<String, String> options, String mapName) { - if (options == null || options.isEmpty()) { + if (options.isEmpty()) { return; } options.forEach( diff --git a/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java b/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java index b03541c567..bcd8e3a780 100644 --- a/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java +++ b/api/src/test/java/org/apache/gravitino/policy/TestPolicyContents.java @@ -177,26 +177,29 @@ public class TestPolicyContents { IllegalArgumentException exception = Assertions.assertThrows(IllegalArgumentException.class, content::validate); - Assertions.assertTrue(exception.g @Test - void testValidateRejectsNegativeMinIntervalMsOnExecutableContent() { - PolicyContent content = - PolicyContents.icebergDataCompaction( - IcebergDataCompactionContent.DEFAULT_MIN_DATA_FILE_MSE, - IcebergDataCompactionContent.DEFAULT_MIN_DELETE_FILE_NUMBER, - IcebergDataCompactionContent.DEFAULT_DATA_FILE_MSE_WEIGHT, - IcebergDataCompactionContent.DEFAULT_DELETE_FILE_NUMBER_WEIGHT, - IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM, - IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY, - IcebergDataCompactionContent.DEFAULT_SORT_ORDER, - null, - null, - null, - -1L, - null, - null); + Assertions.assertTrue(exception.getMessage().contains("maxPartitionNum")); + } + @Test + void testOfRejectsNegativeMinIntervalMsOnExecutableContent() { IllegalArgumentException exception = - Assertions.assertThrows(IllegalArgumentException.class, content::validate); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> + PolicyContents.icebergDataCompaction( + IcebergDataCompactionContent.DEFAULT_MIN_DATA_FILE_MSE, + IcebergDataCompactionContent.DEFAULT_MIN_DELETE_FILE_NUMBER, + IcebergDataCompactionContent.DEFAULT_DATA_FILE_MSE_WEIGHT, + IcebergDataCompactionContent.DEFAULT_DELETE_FILE_NUMBER_WEIGHT, + IcebergDataCompactionContent.DEFAULT_MAX_PARTITION_NUM, + IcebergDataCompactionContent.DEFAULT_REWRITE_STRATEGY, + IcebergDataCompactionContent.DEFAULT_SORT_ORDER, + null, + null, + null, + -1L, + null, + null)); Assertions.assertTrue(exception.getMessage().contains("minIntervalMs")); } @@ -241,9 +244,6 @@ public class TestPolicyContents { Assertions.assertTrue(exception.getMessage().contains("commitTrigger")); } -etMessage().contains("maxPartitionNum")); - } - @Test void testIcebergCompactionContentRejectsUnknownOrInvalidRewriteOptions() { IllegalArgumentException unknown = diff --git a/api/src/test/java/org/apache/gravitino/policy/TestPolicyExecutionSpec.java b/api/src/test/java/org/apache/gravitino/policy/TestPolicyExecutionSpec.java index cf36a06d66..07bff89f56 100644 --- a/api/src/test/java/org/apache/gravitino/policy/TestPolicyExecutionSpec.java +++ b/api/src/test/java/org/apache/gravitino/policy/TestPolicyExecutionSpec.java @@ -58,31 +58,35 @@ public class TestPolicyExecutionSpec { } @Test - void validateAcceptsScalarTriggers() { + void ofAcceptsScalarTriggers() { PolicyExecutionSpec spec = - PolicyExecutionSpec.of( - "0 0 2 * * ?", - true, - 100L, - ImmutableMap.of("uri", "http://irc"), - ImmutableMap.of("metalake", "ml")); - Assertions.assertDoesNotThrow(spec::validate); + Assertions.assertDoesNotThrow( + () -> + PolicyExecutionSpec.of( + "0 0 2 * * ?", + true, + 100L, + ImmutableMap.of("uri", "http://irc"), + ImmutableMap.of("metalake", "ml"))); + Assertions.assertEquals("0 0 2 * * ?", spec.timeTrigger()); + Assertions.assertEquals(Boolean.TRUE, spec.commitTrigger()); } @Test - void validateAcceptsCommitTriggerTrueAlone() { - PolicyExecutionSpec spec = - PolicyExecutionSpec.of(null, true, null, Collections.emptyMap(), Collections.emptyMap()); - Assertions.assertDoesNotThrow(spec::validate); + void ofAcceptsCommitTriggerTrueAlone() { + Assertions.assertDoesNotThrow( + () -> + PolicyExecutionSpec.of( + null, true, null, Collections.emptyMap(), Collections.emptyMap())); } @Test void doesNotParseTimeTriggerCron() { // Cron syntax is owned by the scheduler registration path, not this API type. - PolicyExecutionSpec spec = - PolicyExecutionSpec.of( - "0 2 * *", null, null, Collections.emptyMap(), Collections.emptyMap()); - Assertions.assertDoesNotThrow(spec::validate); + Assertions.assertDoesNotThrow( + () -> + PolicyExecutionSpec.of( + "0 2 * *", null, null, Collections.emptyMap(), Collections.emptyMap())); } @Test @@ -90,14 +94,23 @@ public class TestPolicyExecutionSpec { Assertions.assertSame( PolicyExecutionSpec.EMPTY, PolicyExecutionSpec.of(" ", null, null, Collections.emptyMap(), Collections.emptyMap())); - Assertions.assertDoesNotThrow(PolicyExecutionSpec.EMPTY::validate); } @Test - void validateRejectsNegativeMinInterval() { - PolicyExecutionSpec spec = PolicyExecutionSpec.of(null, null, -1L, null, null); + void ofRejectsNegativeMinInterval() { IllegalArgumentException ex = - Assertions.assertThrows(IllegalArgumentException.class, spec::validate); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> PolicyExecutionSpec.of(null, null, -1L, null, null)); Assertions.assertTrue(ex.getMessage().contains("minIntervalMs")); } + + @Test + void ofRejectsBlankOptionValue() { + IllegalArgumentException ex = + Assertions.assertThrows( + IllegalArgumentException.class, + () -> PolicyExecutionSpec.of(null, null, null, ImmutableMap.of("uri", " "), null)); + Assertions.assertTrue(ex.getMessage().contains("jobOptions")); + } } diff --git a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java index 990c51cf29..7e10253286 100644 --- a/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java +++ b/common/src/main/java/org/apache/gravitino/dto/policy/PolicyContentDTO.java @@ -58,88 +58,63 @@ public interface PolicyContentDTO extends PolicyContent { /** * Optional maintenance execution peers on Iceberg TMS policy content DTOs. * - * <p>Peers are stored as a composed {@link PolicyExecutionSpec}. Flat JSON keys are exposed via - * {@code @JsonProperty} accessors so the wire contract stays flat without duplicating five fields - * on every DTO. + * <p>Implementations store flat peer fields (same JSON keys as the wire contract). {@link + * #executionSpec()} assembles them into a {@link PolicyExecutionSpec} at the DTO/domain boundary. * - * <p>Option maps always return unmodifiable defensive copies so callers cannot mutate DTO state - * after validation or change Lombok equality/hash state through the returned map. + * <p>{@link #jobOptions()} / {@link #updaterOptions()} return unmodifiable defensive copies so + * callers cannot mutate DTO state after validation or change Lombok equality through the returned + * map. */ interface ExecutionPeersDTO { /** - * Stores {@code null} when every peer is unset so Lombok equality matches Jackson round-trips. - * - * @param spec peers to store; empty specs are normalized to {@code null} - * @return {@code null} when unset, otherwise the spec + * @return optional cron expression, or {@code null} when unset */ - static PolicyExecutionSpec normalize(@Nullable PolicyExecutionSpec spec) { - return spec == null || !spec.hasAny() ? null : spec; - } + @Nullable + String timeTrigger(); /** - * @return immutable execution peers; never {@code null} + * @return {@code true}/{@code false} when explicitly set, or {@code null} when unset */ - @JsonIgnore - PolicyExecutionSpec executionSpec(); + @Nullable + Boolean commitTrigger(); /** - * Optional cron expression for automated evaluation. - * - * @return cron string, or {@code null} when unset + * @return cooldown milliseconds, or {@code null} when unset */ @Nullable - @JsonProperty("timeTrigger") - @JsonInclude(JsonInclude.Include.NON_NULL) - default String timeTrigger() { - return executionSpec().timeTrigger(); - } + Long minIntervalMs(); /** - * Optional flag to evaluate the policy after an Iceberg commit. - * - * @return {@code true} when enabled, {@code false} when explicitly disabled, or {@code null} - * when unset + * @return unmodifiable defensive copy of job options; never {@code null} */ - @Nullable - @JsonProperty("commitTrigger") - @JsonInclude(JsonInclude.Include.NON_NULL) - default Boolean commitTrigger() { - return executionSpec().commitTrigger(); - } + Map<String, String> jobOptions(); /** - * Optional cooldown between finished automated runs. - * - * @return milliseconds, or {@code null} when unset + * @return unmodifiable defensive copy of updater options; never {@code null} */ - @Nullable - @JsonProperty("minIntervalMs") - @JsonInclude(JsonInclude.Include.NON_NULL) - default Long minIntervalMs() { - return executionSpec().minIntervalMs(); - } + Map<String, String> updaterOptions(); /** - * Optional non-secret job / client options. - * - * @return unmodifiable defensive copy; never {@code null} (empty when unset) + * @return immutable execution peers assembled from flat fields; never {@code null} */ - @JsonProperty("jobOptions") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - default Map<String, String> jobOptions() { - return executionSpec().jobOptions(); + @JsonIgnore + default PolicyExecutionSpec executionSpec() { + return PolicyExecutionSpec.of( + timeTrigger(), commitTrigger(), minIntervalMs(), jobOptions(), updaterOptions()); } /** - * Optional non-secret updater / control-plane options. + * Defensive copy for option-map accessors. * - * @return unmodifiable defensive copy; never {@code null} (empty when unset) + * @param options raw field value + * @return unmodifiable copy; empty when {@code null} or empty */ - @JsonProperty("updaterOptions") - @JsonInclude(JsonInclude.Include.NON_EMPTY) - default Map<String, String> updaterOptions() { - return executionSpec().updaterOptions(); + static Map<String, String> unmodifiableOptions(@Nullable Map<String, String> options) { + if (options == null || options.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(new LinkedHashMap<>(options)); } } @@ -189,7 +164,7 @@ public interface PolicyContentDTO extends PolicyContent { /** Represents a typed iceberg compaction policy content DTO. */ @EqualsAndHashCode - @ToString(exclude = {"executionSpec", "rewriteOptions"}) + @ToString(exclude = {"rewriteOptions", "jobOptions", "updaterOptions"}) @Builder(setterPrefix = "with") @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) class IcebergCompactionContentDTO implements PolicyContentDTO, ExecutionPeersDTO { @@ -218,12 +193,33 @@ public interface PolicyContentDTO extends PolicyContent { @JsonProperty("rewriteOptions") private Map<String, String> rewriteOptions; - @EqualsAndHashCode.Exclude @JsonIgnore @Nullable private PolicyExecutionSpec executionSpec; + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("timeTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String timeTrigger; - @EqualsAndHashCode.Include - private PolicyExecutionSpec executionSpecForEquals() { - return ExecutionPeersDTO.normalize(executionSpec); - } + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("commitTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean commitTrigger; + + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("minIntervalMs") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Long minIntervalMs; + + @EqualsAndHashCode.Exclude + @JsonProperty("jobOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> jobOptions; + + @EqualsAndHashCode.Exclude + @JsonProperty("updaterOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> updaterOptions; // Default constructor for Jackson deserialization only. private IcebergCompactionContentDTO() {} @@ -315,38 +311,37 @@ public interface PolicyContentDTO extends PolicyContent { } @Override - @JsonIgnore - public PolicyExecutionSpec executionSpec() { - return executionSpec == null ? PolicyExecutionSpec.empty() : executionSpec; + @Nullable + public String timeTrigger() { + return timeTrigger; } - @JsonProperty("timeTrigger") - private void setTimeTrigger(String timeTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withTimeTrigger(timeTrigger)); + @Override + @Nullable + public Boolean commitTrigger() { + return commitTrigger; } - @JsonProperty("commitTrigger") - private void setCommitTrigger(Boolean commitTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withCommitTrigger(commitTrigger)); + @Override + @Nullable + public Long minIntervalMs() { + return minIntervalMs; } - @JsonProperty("minIntervalMs") - private void setMinIntervalMs(Long minIntervalMs) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withMinIntervalMs(minIntervalMs)); + @Override + public Map<String, String> jobOptions() { + return ExecutionPeersDTO.unmodifiableOptions(jobOptions); } - @JsonProperty("jobOptions") - private void setJobOptions(Map<String, String> jobOptions) { - this.executionSpec = ExecutionPeersDTO.normalize(executionSpec().withJobOptions(jobOptions)); + @Override + public Map<String, String> updaterOptions() { + return ExecutionPeersDTO.unmodifiableOptions(updaterOptions); } - @JsonProperty("updaterOptions") - private void setUpdaterOptions(Map<String, String> updaterOptions) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withUpdaterOptions(updaterOptions)); + @EqualsAndHashCode.Include + private PolicyExecutionSpec executionSpecForEquals() { + return PolicyExecutionSpec.of( + timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); } @Override @@ -390,11 +385,37 @@ public interface PolicyContentDTO extends PolicyContent { spec.jobOptions(), spec.updaterOptions()); } + + /** Lombok builder customization: unpack PolicyExecutionSpec into flat peer fields. */ + public static class IcebergCompactionContentDTOBuilder { + /** + * @param executionSpec peers to copy; {@code null} or empty leaves peer fields unset + * @return this builder + */ + public IcebergCompactionContentDTOBuilder withExecutionSpec( + @Nullable PolicyExecutionSpec executionSpec) { + if (executionSpec == null || !executionSpec.hasAny()) { + return this; + } + this.timeTrigger = executionSpec.timeTrigger(); + this.commitTrigger = executionSpec.commitTrigger(); + this.minIntervalMs = executionSpec.minIntervalMs(); + this.jobOptions = + executionSpec.jobOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.jobOptions()); + this.updaterOptions = + executionSpec.updaterOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.updaterOptions()); + return this; + } + } } /** Typed orphan cleanup policy content for REST requests and responses. */ @EqualsAndHashCode - @ToString(exclude = {"executionSpec"}) + @ToString(exclude = {"jobOptions", "updaterOptions"}) @Builder(setterPrefix = "with") @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) class IcebergOrphanFileRemovalContentDTO implements PolicyContentDTO, ExecutionPeersDTO { @@ -407,12 +428,33 @@ public interface PolicyContentDTO extends PolicyContent { @JsonProperty("dryRun") private Boolean dryRun; - @EqualsAndHashCode.Exclude @JsonIgnore @Nullable private PolicyExecutionSpec executionSpec; + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("timeTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String timeTrigger; - @EqualsAndHashCode.Include - private PolicyExecutionSpec executionSpecForEquals() { - return ExecutionPeersDTO.normalize(executionSpec); - } + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("commitTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean commitTrigger; + + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("minIntervalMs") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Long minIntervalMs; + + @EqualsAndHashCode.Exclude + @JsonProperty("jobOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> jobOptions; + + @EqualsAndHashCode.Exclude + @JsonProperty("updaterOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> updaterOptions; private IcebergOrphanFileRemovalContentDTO() {} @@ -441,38 +483,37 @@ public interface PolicyContentDTO extends PolicyContent { } @Override - @JsonIgnore - public PolicyExecutionSpec executionSpec() { - return executionSpec == null ? PolicyExecutionSpec.empty() : executionSpec; + @Nullable + public String timeTrigger() { + return timeTrigger; } - @JsonProperty("timeTrigger") - private void setTimeTrigger(String timeTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withTimeTrigger(timeTrigger)); + @Override + @Nullable + public Boolean commitTrigger() { + return commitTrigger; } - @JsonProperty("commitTrigger") - private void setCommitTrigger(Boolean commitTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withCommitTrigger(commitTrigger)); + @Override + @Nullable + public Long minIntervalMs() { + return minIntervalMs; } - @JsonProperty("minIntervalMs") - private void setMinIntervalMs(Long minIntervalMs) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withMinIntervalMs(minIntervalMs)); + @Override + public Map<String, String> jobOptions() { + return ExecutionPeersDTO.unmodifiableOptions(jobOptions); } - @JsonProperty("jobOptions") - private void setJobOptions(Map<String, String> jobOptions) { - this.executionSpec = ExecutionPeersDTO.normalize(executionSpec().withJobOptions(jobOptions)); + @Override + public Map<String, String> updaterOptions() { + return ExecutionPeersDTO.unmodifiableOptions(updaterOptions); } - @JsonProperty("updaterOptions") - private void setUpdaterOptions(Map<String, String> updaterOptions) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withUpdaterOptions(updaterOptions)); + @EqualsAndHashCode.Include + private PolicyExecutionSpec executionSpecForEquals() { + return PolicyExecutionSpec.of( + timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); } @Override @@ -514,11 +555,37 @@ public interface PolicyContentDTO extends PolicyContent { spec.jobOptions(), spec.updaterOptions()); } + + /** Lombok builder customization: unpack PolicyExecutionSpec into flat peer fields. */ + public static class IcebergOrphanFileRemovalContentDTOBuilder { + /** + * @param executionSpec peers to copy; {@code null} or empty leaves peer fields unset + * @return this builder + */ + public IcebergOrphanFileRemovalContentDTOBuilder withExecutionSpec( + @Nullable PolicyExecutionSpec executionSpec) { + if (executionSpec == null || !executionSpec.hasAny()) { + return this; + } + this.timeTrigger = executionSpec.timeTrigger(); + this.commitTrigger = executionSpec.commitTrigger(); + this.minIntervalMs = executionSpec.minIntervalMs(); + this.jobOptions = + executionSpec.jobOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.jobOptions()); + this.updaterOptions = + executionSpec.updaterOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.updaterOptions()); + return this; + } + } } /** Typed manifest rewrite policy content. */ @EqualsAndHashCode - @ToString(exclude = {"executionSpec"}) + @ToString(exclude = {"jobOptions", "updaterOptions"}) @Builder(setterPrefix = "with") @AllArgsConstructor(access = AccessLevel.PRIVATE) class IcebergRewriteManifestsContentDTO implements PolicyContentDTO, ExecutionPeersDTO { @@ -542,12 +609,33 @@ public interface PolicyContentDTO extends PolicyContent { @JsonDeserialize(using = ManifestUseCachingDeserializer.class) private Boolean useCaching; - @EqualsAndHashCode.Exclude @JsonIgnore @Nullable private PolicyExecutionSpec executionSpec; + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("timeTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private String timeTrigger; - @EqualsAndHashCode.Include - private PolicyExecutionSpec executionSpecForEquals() { - return ExecutionPeersDTO.normalize(executionSpec); - } + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("commitTrigger") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean commitTrigger; + + @EqualsAndHashCode.Exclude + @Nullable + @JsonProperty("minIntervalMs") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Long minIntervalMs; + + @EqualsAndHashCode.Exclude + @JsonProperty("jobOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> jobOptions; + + @EqualsAndHashCode.Exclude + @JsonProperty("updaterOptions") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private Map<String, String> updaterOptions; private IcebergRewriteManifestsContentDTO() {} @@ -614,38 +702,37 @@ public interface PolicyContentDTO extends PolicyContent { } @Override - @JsonIgnore - public PolicyExecutionSpec executionSpec() { - return executionSpec == null ? PolicyExecutionSpec.empty() : executionSpec; + @Nullable + public String timeTrigger() { + return timeTrigger; } - @JsonProperty("timeTrigger") - private void setTimeTrigger(String timeTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withTimeTrigger(timeTrigger)); + @Override + @Nullable + public Boolean commitTrigger() { + return commitTrigger; } - @JsonProperty("commitTrigger") - private void setCommitTrigger(Boolean commitTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withCommitTrigger(commitTrigger)); + @Override + @Nullable + public Long minIntervalMs() { + return minIntervalMs; } - @JsonProperty("minIntervalMs") - private void setMinIntervalMs(Long minIntervalMs) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withMinIntervalMs(minIntervalMs)); + @Override + public Map<String, String> jobOptions() { + return ExecutionPeersDTO.unmodifiableOptions(jobOptions); } - @JsonProperty("jobOptions") - private void setJobOptions(Map<String, String> jobOptions) { - this.executionSpec = ExecutionPeersDTO.normalize(executionSpec().withJobOptions(jobOptions)); + @Override + public Map<String, String> updaterOptions() { + return ExecutionPeersDTO.unmodifiableOptions(updaterOptions); } - @JsonProperty("updaterOptions") - private void setUpdaterOptions(Map<String, String> updaterOptions) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withUpdaterOptions(updaterOptions)); + @EqualsAndHashCode.Include + private PolicyExecutionSpec executionSpecForEquals() { + return PolicyExecutionSpec.of( + timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); } @Override @@ -723,6 +810,32 @@ public interface PolicyContentDTO extends PolicyContent { spec.jobOptions(), spec.updaterOptions()); } + + /** Lombok builder customization: unpack PolicyExecutionSpec into flat peer fields. */ + public static class IcebergRewriteManifestsContentDTOBuilder { + /** + * @param executionSpec peers to copy; {@code null} or empty leaves peer fields unset + * @return this builder + */ + public IcebergRewriteManifestsContentDTOBuilder withExecutionSpec( + @Nullable PolicyExecutionSpec executionSpec) { + if (executionSpec == null || !executionSpec.hasAny()) { + return this; + } + this.timeTrigger = executionSpec.timeTrigger(); + this.commitTrigger = executionSpec.commitTrigger(); + this.minIntervalMs = executionSpec.minIntervalMs(); + this.jobOptions = + executionSpec.jobOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.jobOptions()); + this.updaterOptions = + executionSpec.updaterOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.updaterOptions()); + return this; + } + } } /** @@ -776,38 +889,37 @@ public interface PolicyContentDTO extends PolicyContent { } @Override - @JsonIgnore - public PolicyExecutionSpec executionSpec() { - return executionSpec == null ? PolicyExecutionSpec.empty() : executionSpec; + @Nullable + public String timeTrigger() { + return timeTrigger; } - @JsonProperty("timeTrigger") - private void setTimeTrigger(String timeTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withTimeTrigger(timeTrigger)); + @Override + @Nullable + public Boolean commitTrigger() { + return commitTrigger; } - @JsonProperty("commitTrigger") - private void setCommitTrigger(Boolean commitTrigger) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withCommitTrigger(commitTrigger)); + @Override + @Nullable + public Long minIntervalMs() { + return minIntervalMs; } - @JsonProperty("minIntervalMs") - private void setMinIntervalMs(Long minIntervalMs) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withMinIntervalMs(minIntervalMs)); + @Override + public Map<String, String> jobOptions() { + return ExecutionPeersDTO.unmodifiableOptions(jobOptions); } - @JsonProperty("jobOptions") - private void setJobOptions(Map<String, String> jobOptions) { - this.executionSpec = ExecutionPeersDTO.normalize(executionSpec().withJobOptions(jobOptions)); + @Override + public Map<String, String> updaterOptions() { + return ExecutionPeersDTO.unmodifiableOptions(updaterOptions); } - @JsonProperty("updaterOptions") - private void setUpdaterOptions(Map<String, String> updaterOptions) { - this.executionSpec = - ExecutionPeersDTO.normalize(executionSpec().withUpdaterOptions(updaterOptions)); + @EqualsAndHashCode.Include + private PolicyExecutionSpec executionSpecForEquals() { + return PolicyExecutionSpec.of( + timeTrigger, commitTrigger, minIntervalMs, jobOptions, updaterOptions); } @Override @@ -860,6 +972,32 @@ public interface PolicyContentDTO extends PolicyContent { return PolicyContents.icebergSnapshotExpiration( olderThanDays(), retainLast(), streamResults()); } + + /** Lombok builder customization: unpack PolicyExecutionSpec into flat peer fields. */ + public static class IcebergSnapshotExpirationContentDTOBuilder { + /** + * @param executionSpec peers to copy; {@code null} or empty leaves peer fields unset + * @return this builder + */ + public IcebergSnapshotExpirationContentDTOBuilder withExecutionSpec( + @Nullable PolicyExecutionSpec executionSpec) { + if (executionSpec == null || !executionSpec.hasAny()) { + return this; + } + this.timeTrigger = executionSpec.timeTrigger(); + this.commitTrigger = executionSpec.commitTrigger(); + this.minIntervalMs = executionSpec.minIntervalMs(); + this.jobOptions = + executionSpec.jobOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.jobOptions()); + this.updaterOptions = + executionSpec.updaterOptions().isEmpty() + ? null + : new LinkedHashMap<>(executionSpec.updaterOptions()); + return this; + } + } } /** Represents typed row-filter policy content. */
