diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContent.java similarity index 94% rename from polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java rename to polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContent.java index a1e82f7b6a..21186f6c70 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyContent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContent.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy; +package org.apache.polaris.core.policy.content; /** A marker interface for policy content */ public interface PolicyContent {} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContentUtil.java similarity index 94% rename from polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java rename to polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContentUtil.java index a08cdfd7e7..2ba025a663 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidatorUtil.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/PolicyContentUtil.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy.validator; +package org.apache.polaris.core.policy.content; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -public class PolicyValidatorUtil { +public class PolicyContentUtil { public static final ObjectMapper MAPPER = configureMapper(); private static ObjectMapper configureMapper() { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/StrictBooleanDeserializer.java similarity index 92% rename from polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java rename to polaris-core/src/main/java/org/apache/polaris/core/policy/content/StrictBooleanDeserializer.java index f6da87e702..3c54d10d1d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/StrictBooleanDeserializer.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/StrictBooleanDeserializer.java @@ -16,12 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy.validator; +package org.apache.polaris.core.policy.content; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import java.io.IOException; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; public class StrictBooleanDeserializer extends JsonDeserializer { @Override diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/BaseMaintenancePolicyContent.java similarity index 57% rename from polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java rename to polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/BaseMaintenancePolicyContent.java index efdd158b5d..a825e87ba3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyContent.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/BaseMaintenancePolicyContent.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy.validator.datacompaction; +package org.apache.polaris.core.policy.content.maintenance; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -24,15 +24,11 @@ import com.google.common.base.Strings; import java.util.Map; import java.util.Set; -import org.apache.polaris.core.policy.PolicyContent; +import org.apache.polaris.core.policy.content.PolicyContent; +import org.apache.polaris.core.policy.content.StrictBooleanDeserializer; import org.apache.polaris.core.policy.validator.InvalidPolicyException; -import org.apache.polaris.core.policy.validator.PolicyValidatorUtil; -import org.apache.polaris.core.policy.validator.StrictBooleanDeserializer; - -public class DataCompactionPolicyContent implements PolicyContent { - private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; - private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); +public abstract class BaseMaintenancePolicyContent implements PolicyContent { @JsonDeserialize(using = StrictBooleanDeserializer.class) private Boolean enable; @@ -40,7 +36,7 @@ public class DataCompactionPolicyContent implements PolicyContent { private Map config; @JsonCreator - public DataCompactionPolicyContent( + public BaseMaintenancePolicyContent( @JsonProperty(value = "enable", required = true) boolean enable) { this.enable = enable; } @@ -69,29 +65,21 @@ public void setConfig(Map config) { this.config = config; } - public static DataCompactionPolicyContent fromString(String content) { - if (Strings.isNullOrEmpty(content)) { - throw new InvalidPolicyException("Policy is empty"); + static void validateVersion( + String content, + BaseMaintenancePolicyContent policy, + String defaultVersion, + Set allVersions) { + if (policy == null) { + throw new InvalidPolicyException("Invalid policy: " + content); } - try { - DataCompactionPolicyContent policy = - PolicyValidatorUtil.MAPPER.readValue(content, DataCompactionPolicyContent.class); - if (policy == null) { - throw new InvalidPolicyException("Invalid policy"); - } - - if (Strings.isNullOrEmpty(policy.getVersion())) { - policy.setVersion(DEFAULT_POLICY_SCHEMA_VERSION); - } - - if (!POLICY_SCHEMA_VERSIONS.contains(policy.getVersion())) { - throw new InvalidPolicyException("Invalid policy version: " + policy.getVersion()); - } + if (Strings.isNullOrEmpty(policy.getVersion())) { + policy.setVersion(defaultVersion); + } - return policy; - } catch (Exception e) { - throw new InvalidPolicyException(e); + if (!allVersions.contains(policy.getVersion())) { + throw new InvalidPolicyException("Invalid policy version: " + policy.getVersion()); } } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/DataCompactionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/DataCompactionPolicyContent.java new file mode 100644 index 0000000000..a5a4012c49 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/DataCompactionPolicyContent.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy.content.maintenance; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.Set; +import org.apache.polaris.core.policy.content.PolicyContentUtil; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; + +public class DataCompactionPolicyContent extends BaseMaintenancePolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonCreator + public DataCompactionPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + super(enable); + } + + public static DataCompactionPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + DataCompactionPolicyContent policy; + try { + policy = PolicyContentUtil.MAPPER.readValue(content, DataCompactionPolicyContent.class); + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + + validateVersion(content, policy, DEFAULT_POLICY_SCHEMA_VERSION, POLICY_SCHEMA_VERSIONS); + + return policy; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/MetadataCompactionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/MetadataCompactionPolicyContent.java new file mode 100644 index 0000000000..a22ca2d527 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/MetadataCompactionPolicyContent.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy.content.maintenance; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.Set; +import org.apache.polaris.core.policy.content.PolicyContentUtil; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; + +public class MetadataCompactionPolicyContent extends BaseMaintenancePolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonCreator + public MetadataCompactionPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + super(enable); + } + + public static MetadataCompactionPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + MetadataCompactionPolicyContent policy; + try { + policy = PolicyContentUtil.MAPPER.readValue(content, MetadataCompactionPolicyContent.class); + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + + validateVersion(content, policy, DEFAULT_POLICY_SCHEMA_VERSION, POLICY_SCHEMA_VERSIONS); + + return policy; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/OrphanFileRemovalPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/OrphanFileRemovalPolicyContent.java new file mode 100644 index 0000000000..0adf0ed505 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/OrphanFileRemovalPolicyContent.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy.content.maintenance; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.List; +import java.util.Set; +import org.apache.polaris.core.policy.content.PolicyContentUtil; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; + +public class OrphanFileRemovalPolicyContent extends BaseMaintenancePolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonProperty(value = "max_orphan_file_age_in_days") + private int maxOrphanFileAgeInDays; + + private List locations; + + @JsonCreator + public OrphanFileRemovalPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + super(enable); + } + + public int getMaxOrphanFileAgeInDays() { + return maxOrphanFileAgeInDays; + } + + public void setMaxOrphanFileAgeInDays(int maxOrphanFileAgeInDays) { + this.maxOrphanFileAgeInDays = maxOrphanFileAgeInDays; + } + + public List getLocations() { + return locations; + } + + public void setLocations(List locations) { + this.locations = locations; + } + + public static OrphanFileRemovalPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + OrphanFileRemovalPolicyContent policy; + try { + policy = PolicyContentUtil.MAPPER.readValue(content, OrphanFileRemovalPolicyContent.class); + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + + validateVersion(content, policy, DEFAULT_POLICY_SCHEMA_VERSION, POLICY_SCHEMA_VERSIONS); + + int maxAge = policy.getMaxOrphanFileAgeInDays(); + if (maxAge < 0) { + throw new InvalidPolicyException( + "Invalid max_orphan_file_age_in_days: " + + maxAge + + ". It must be greater than or equal to 0"); + } + + return policy; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/SnapshotRetentionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/SnapshotRetentionPolicyContent.java new file mode 100644 index 0000000000..ecb3e3d6c2 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/maintenance/SnapshotRetentionPolicyContent.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy.content.maintenance; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import java.util.Set; +import org.apache.polaris.core.policy.content.PolicyContentUtil; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; + +public class SnapshotRetentionPolicyContent extends BaseMaintenancePolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-02-03"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonCreator + public SnapshotRetentionPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + super(enable); + } + + public static SnapshotRetentionPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + SnapshotRetentionPolicyContent policy; + try { + policy = PolicyContentUtil.MAPPER.readValue(content, SnapshotRetentionPolicyContent.class); + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + + validateVersion(content, policy, DEFAULT_POLICY_SCHEMA_VERSION, POLICY_SCHEMA_VERSIONS); + + return policy; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java index ed37f12792..3ccb2f6d2e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java @@ -22,7 +22,11 @@ import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.policy.PolicyEntity; import org.apache.polaris.core.policy.PredefinedPolicyTypes; -import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator; +import org.apache.polaris.core.policy.content.maintenance.DataCompactionPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.MetadataCompactionPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.OrphanFileRemovalPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.SnapshotRetentionPolicyContent; +import org.apache.polaris.core.policy.validator.maintenance.BaseMaintenancePolicyValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,15 +54,19 @@ public static void validate(PolicyEntity policy) { switch (type) { case DATA_COMPACTION: - DataCompactionPolicyValidator.INSTANCE.validate(policy.getContent()); + DataCompactionPolicyContent.fromString(policy.getContent()); break; - - // To support additional policy types in the future, add cases here. case METADATA_COMPACTION: + MetadataCompactionPolicyContent.fromString(policy.getContent()); + break; case SNAPSHOT_RETENTION: + SnapshotRetentionPolicyContent.fromString(policy.getContent()); + break; case ORPHAN_FILE_REMOVAL: + OrphanFileRemovalPolicyContent.fromString(policy.getContent()); + break; default: - throw new InvalidPolicyException("Unsupported policy type: " + type.getName()); + throw new IllegalArgumentException("Unsupported policy type: " + type.getName()); } LOGGER.info("Policy validated successfully: {}", type.getName()); @@ -79,14 +87,16 @@ public static boolean canAttach(PolicyEntity policy, PolarisEntity targetEntity) Preconditions.checkArgument( policyType != null, "Unknown policy type: " + policy.getPolicyTypeCode()); + var entityType = targetEntity.getType(); + var entitySubType = targetEntity.getSubType(); + switch (policyType) { case DATA_COMPACTION: - return DataCompactionPolicyValidator.INSTANCE.canAttach( - targetEntity.getType(), targetEntity.getSubType()); - // To support additional policy types in the future, add cases here. case METADATA_COMPACTION: case SNAPSHOT_RETENTION: case ORPHAN_FILE_REMOVAL: + return BaseMaintenancePolicyValidator.INSTANCE.canAttach(entityType, entitySubType); + default: LOGGER.warn("Attachment not supported for policy type: {}", policyType.getName()); return false; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java similarity index 86% rename from polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java rename to polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java index 344c72d909..86308f5559 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/datacompaction/DataCompactionPolicyValidator.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy.validator.datacompaction; +package org.apache.polaris.core.policy.validator.maintenance; import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; @@ -28,16 +28,15 @@ import org.apache.polaris.core.policy.validator.InvalidPolicyException; import org.apache.polaris.core.policy.validator.PolicyValidator; -public class DataCompactionPolicyValidator implements PolicyValidator { - public static final DataCompactionPolicyValidator INSTANCE = new DataCompactionPolicyValidator(); +public class BaseMaintenancePolicyValidator implements PolicyValidator { + public static final BaseMaintenancePolicyValidator INSTANCE = + new BaseMaintenancePolicyValidator(); private static final Set ATTACHABLE_ENTITY_TYPES = Set.of(CATALOG, NAMESPACE, TABLE_LIKE); @Override - public void validate(String content) throws InvalidPolicyException { - DataCompactionPolicyContent.fromString(content); - } + public void validate(String content) throws InvalidPolicyException {} @Override public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) { diff --git a/polaris-core/src/main/resources/schemas/policies/system/orphan-file-removal/2025-02-03.json b/polaris-core/src/main/resources/schemas/policies/system/orphan-file-removal/2025-02-03.json index 19f35deca3..f7b00d1e4e 100644 --- a/polaris-core/src/main/resources/schemas/policies/system/orphan-file-removal/2025-02-03.json +++ b/polaris-core/src/main/resources/schemas/policies/system/orphan-file-removal/2025-02-03.json @@ -40,7 +40,7 @@ "version": "2025-02-03", "enable": true, "max_orphan_file_age_in_days": 30, - "location": "s3://my-bucket/my-table-location", + "locations": ["s3://my-bucket/my-table-location"], "config": { "prefix_mismatch_mode": "ignore", "key1": "value1" diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/MaintenancePolicyContentTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/MaintenancePolicyContentTest.java new file mode 100644 index 0000000000..1b5972a05f --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/MaintenancePolicyContentTest.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy; + +import static org.apache.polaris.core.policy.content.maintenance.OrphanFileRemovalPolicyContent.fromString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.function.Function; +import java.util.stream.Stream; +import org.apache.polaris.core.policy.content.maintenance.BaseMaintenancePolicyContent; +import org.apache.polaris.core.policy.content.maintenance.DataCompactionPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.MetadataCompactionPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.OrphanFileRemovalPolicyContent; +import org.apache.polaris.core.policy.content.maintenance.SnapshotRetentionPolicyContent; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class MaintenancePolicyContentTest { + static Stream policyTypes() { + return Stream.of( + Arguments.of(PredefinedPolicyTypes.DATA_COMPACTION), + Arguments.of(PredefinedPolicyTypes.METADATA_COMPACTION), + Arguments.of(PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL), + Arguments.of(PredefinedPolicyTypes.METADATA_COMPACTION)); + } + + Function getParser(PredefinedPolicyTypes policyType) { + switch (policyType) { + case DATA_COMPACTION: + return DataCompactionPolicyContent::fromString; + case METADATA_COMPACTION: + return MetadataCompactionPolicyContent::fromString; + case ORPHAN_FILE_REMOVAL: + return OrphanFileRemovalPolicyContent::fromString; + case SNAPSHOT_RETENTION: + return SnapshotRetentionPolicyContent::fromString; + default: + throw new IllegalArgumentException("Unknown policy type: " + policyType); + } + } + + @ParameterizedTest + @MethodSource("policyTypes") + public void testValidPolicyContent(PredefinedPolicyTypes policyType) { + var parser = getParser(policyType); + + assertThat(parser.apply("{\"enable\": false}").enabled()).isFalse(); + assertThat(parser.apply("{\"enable\": true}").enabled()).isTrue(); + + var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}"; + assertThat(parser.apply(validJson).getVersion()).isEqualTo("2025-02-03"); + + validJson = "{\"enable\": true, \"config\": {\"key1\": \"value1\", \"key2\": true}}"; + assertThat(parser.apply(validJson).getConfig().get("key1")).isEqualTo("value1"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidEmptyString(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + assertThatThrownBy(() -> parser.apply("")) + .as("Validating empty string should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Policy is empty"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidJSONLiteralNull(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + assertThatThrownBy(() -> parser.apply("null")) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy: null"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidEmptyJson(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + assertThatThrownBy(() -> parser.apply("{}")) + .as("Validating empty JSON '{}' should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidInvalidVersionFormat(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + String invalidPolicy = "{\"enable\": true, \"version\": \"fdafds\"}"; + assertThatThrownBy(() -> parser.apply(invalidPolicy)) + .as("Validating policy with invalid version format should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidInvalidKeyInPolicy(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + String invalidPolicy = "{\"version\":\"2025-02-03\", \"enable\": true, \"invalid_key\": 12342}"; + assertThatThrownBy(() -> parser.apply(invalidPolicy)) + .as("Validating policy with an unknown key should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidUnrecognizedToken(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + var invalidPolicy = "{\"enable\": invalidToken}"; + assertThatThrownBy(() -> parser.apply(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidNullValue(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + var invalidPolicy = "{\"enable\": null}"; + assertThatThrownBy(() -> parser.apply(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @ParameterizedTest + @MethodSource("policyTypes") + void testIsValidWrongString(PredefinedPolicyTypes policyTypes) { + var parser = getParser(policyTypes); + var invalidPolicy = "{\"enable\": \"invalid\"}"; + assertThatThrownBy(() -> parser.apply(invalidPolicy)) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + public void testValidOrphanFileRemovalPolicyContent() { + assertThat( + fromString("{\"enable\": true, \"max_orphan_file_age_in_days\": 3}") + .getMaxOrphanFileAgeInDays()) + .isEqualTo(3); + assertThat( + fromString( + "{\"enable\": true, \"max_orphan_file_age_in_days\": 3, \"locations\": [" + + " \"s3://my-bucket/ns/my_table/\"," + + " \"s3://my-bucket/ns/my_table/my-data/\"," + + " \"s3://my-bucket/ns/my_table/my-metadata\"" + + " ]}") + .getLocations() + .get(0)) + .isEqualTo("s3://my-bucket/ns/my_table/"); + } + + @Test + public void testInvalidOrphanFileRemovalPolicyContent() { + assertThatThrownBy(() -> fromString("{\"enable\": true, \"max_orphan_file_age_in_days\": -3}")) + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid max_orphan_file_age_in_days"); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyValidatorsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyValidatorsTest.java new file mode 100644 index 0000000000..04435ba734 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyValidatorsTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.policy; + +import static org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_TABLE; +import static org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_VIEW; +import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.entity.CatalogEntity; +import org.apache.polaris.core.entity.NamespaceEntity; +import org.apache.polaris.core.entity.PrincipalEntity; +import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; +import org.apache.polaris.core.policy.validator.PolicyValidators; +import org.junit.jupiter.api.Test; + +public class PolicyValidatorsTest { + Namespace ns = Namespace.of("NS1"); + TableIdentifier tableIdentifier = TableIdentifier.of(ns, "table1"); + PolicyEntity policyEntity = new PolicyEntity.Builder(ns, "pn", DATA_COMPACTION).build(); + + @Test + public void testInvalidPolicy() { + var policyEntity = + new PolicyEntity.Builder(ns, "testPolicy", DATA_COMPACTION) + .setContent("InvalidContent") + .setPolicyVersion(0) + .build(); + assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) + .as("Validating empty JSON '{}' should throw InvalidPolicyException") + .isInstanceOf(InvalidPolicyException.class) + .hasMessageContaining("Invalid policy"); + } + + @Test + public void testUnsupportedPolicyType() { + var newPolicyType = + new PolicyType() { + @Override + public int getCode() { + return Integer.MAX_VALUE; + } + + @Override + public String getName() { + return ""; + } + + @Override + public boolean isInheritable() { + return false; + } + }; + + var policyEntity = new PolicyEntity.Builder(ns, "testPolicy", newPolicyType).build(); + + assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown policy type:"); + } + + @Test + public void testValidPolicy() { + var policyEntity = + new PolicyEntity.Builder(ns, "testPolicy", DATA_COMPACTION) + .setContent("{\"enable\": false}") + .setPolicyVersion(0) + .build(); + PolicyValidators.validate(policyEntity); + } + + @Test + public void testCanAttachReturnsTrueForCatalogType() { + var targetEntity = new CatalogEntity.Builder().build(); + var result = PolicyValidators.canAttach(policyEntity, targetEntity); + assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); + } + + @Test + public void testCanAttachReturnsTrueForNamespaceType() { + var targetEntity = new NamespaceEntity.Builder(ns).build(); + var result = PolicyValidators.canAttach(policyEntity, targetEntity); + assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); + } + + @Test + public void testCanAttachReturnsTrueForIcebergTableLikeWithTableSubtype() { + var targetEntity = + new IcebergTableLikeEntity.Builder(tableIdentifier, "").setSubType(ICEBERG_TABLE).build(); + var result = PolicyValidators.canAttach(policyEntity, targetEntity); + assertThat(result) + .isTrue() + .as("Expected canAttach() to return true for ICEBERG_TABLE_LIKE with TABLE subtype"); + } + + @Test + public void testCanAttachReturnsFalseForIcebergTableLikeWithNonTableSubtype() { + var targetEntity = + new IcebergTableLikeEntity.Builder(tableIdentifier, "").setSubType(ICEBERG_VIEW).build(); + var result = PolicyValidators.canAttach(policyEntity, targetEntity); + assertThat(result) + .isFalse() + .as("Expected canAttach() to return false for ICEBERG_TABLE_LIKE with non-TABLE subtype"); + } + + @Test + public void testCanAttachReturnsFalseForUnattachableType() { + var targetEntity = new PrincipalEntity.Builder().build(); + var result = PolicyValidators.canAttach(policyEntity, targetEntity); + assertThat(result).isFalse().as("Expected canAttach() to return false for null"); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java deleted file mode 100644 index 3d70fbeeff..0000000000 --- a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyContentTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.policy.validator; - -import static org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyContent.fromString; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Test; - -public class DataCompactionPolicyContentTest { - @Test - public void testValidPolicies() { - assertThat(fromString("{\"enable\": false}").enabled()).isFalse(); - assertThat(fromString("{\"enable\": true}").enabled()).isTrue(); - - var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}"; - assertThat(fromString(validJson).getVersion()).isEqualTo("2025-02-03"); - - validJson = "{\"enable\": true, \"config\": {\"key1\": \"value1\", \"key2\": true}}"; - assertThat(fromString(validJson).getConfig().get("key1")).isEqualTo("value1"); - } - - @Test - void testIsValidEmptyString() { - assertThatThrownBy(() -> fromString("")) - .as("Validating empty string should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Policy is empty"); - } - - @Test - void testIsValidEmptyJson() { - assertThatThrownBy(() -> fromString("{}")) - .as("Validating empty JSON '{}' should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } - - @Test - void testIsValidInvalidVersionFormat() { - String invalidPolicy1 = "{\"enable\": true, \"version\": \"fdafds\"}"; - assertThatThrownBy(() -> fromString(invalidPolicy1)) - .as("Validating policy with invalid version format should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class); - } - - @Test - void testIsValidInvalidKeyInPolicy() { - String invalidPolicy2 = - "{\"version\":\"2025-02-03\", \"enable\": true, \"invalid_key\": 12342}"; - assertThatThrownBy(() -> fromString(invalidPolicy2)) - .as("Validating policy with an unknown key should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } - - @Test - void testIsValidUnrecognizedToken() { - var invalidPolicy = "{\"enable\": invalidToken}"; - assertThatThrownBy(() -> fromString(invalidPolicy)) - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } - - @Test - void testIsValidNullValue() { - var invalidPolicy = "{\"enable\": null}"; - assertThatThrownBy(() -> fromString(invalidPolicy)) - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } - - @Test - void testIsValidWrongString() { - var invalidPolicy = "{\"enable\": \"invalid\"}"; - assertThatThrownBy(() -> fromString(invalidPolicy)) - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } -} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java deleted file mode 100644 index 18205763fa..0000000000 --- a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/DataCompactionPolicyValidatorTest.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.policy.validator; - -import static org.apache.polaris.core.entity.PolarisEntitySubType.ANY_SUBTYPE; -import static org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_TABLE; -import static org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_VIEW; -import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; -import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; -import static org.apache.polaris.core.entity.PolarisEntityType.PRINCIPAL; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.apache.polaris.core.entity.PolarisEntityType; -import org.apache.polaris.core.policy.validator.datacompaction.DataCompactionPolicyValidator; -import org.junit.jupiter.api.Test; - -public class DataCompactionPolicyValidatorTest { - private final DataCompactionPolicyValidator validator = new DataCompactionPolicyValidator(); - - @Test - public void testValidPolicies() { - var validJson = "{\"version\":\"2025-02-03\", \"enable\": true}"; - validator.validate(validJson); - - assertThatThrownBy(() -> validator.validate("")) - .as("Validating empty string should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Policy is empty"); - } - - @Test - public void testCanAttachReturnsTrueForCatalogType() { - var result = validator.canAttach(CATALOG, ANY_SUBTYPE); // using any valid subtype - assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); - } - - @Test - public void testCanAttachReturnsTrueForNamespaceType() { - var result = validator.canAttach(NAMESPACE, ANY_SUBTYPE); // using any valid subtype - assertThat(result).isTrue().as("Expected canAttach() to return true for CATALOG type"); - } - - @Test - public void testCanAttachReturnsTrueForIcebergTableLikeWithTableSubtype() { - var result = validator.canAttach(PolarisEntityType.TABLE_LIKE, ICEBERG_TABLE); - assertThat(result) - .isTrue() - .as("Expected canAttach() to return true for ICEBERG_TABLE_LIKE with TABLE subtype"); - } - - @Test - public void testCanAttachReturnsFalseForIcebergTableLikeWithNonTableSubtype() { - // For ICEBERG_TABLE_LIKE, any subtype other than TABLE should return false. - boolean result = validator.canAttach(PolarisEntityType.TABLE_LIKE, ICEBERG_VIEW); - assertThat(result) - .isFalse() - .as("Expected canAttach() to return false for ICEBERG_TABLE_LIKE with non-TABLE subtype"); - } - - @Test - public void testCanAttachReturnsFalseForNull() { - var result = validator.canAttach(null, null); // using any valid subtype - assertThat(result).isFalse().as("Expected canAttach() to return false for null"); - } - - @Test - public void testCanAttachReturnsFalseForUnattachableType() { - var result = validator.canAttach(PRINCIPAL, null); // using any valid subtype - assertThat(result).isFalse().as("Expected canAttach() to return false for null"); - } -} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java deleted file mode 100644 index 24d122a119..0000000000 --- a/polaris-core/src/test/java/org/apache/polaris/core/policy/validator/PolicyValidatorsTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.core.policy.validator; - -import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION; -import static org.apache.polaris.core.policy.PredefinedPolicyTypes.METADATA_COMPACTION; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.apache.iceberg.catalog.Namespace; -import org.apache.polaris.core.policy.PolicyEntity; -import org.junit.jupiter.api.Test; - -public class PolicyValidatorsTest { - @Test - public void testInvalidPolicy() { - var policyEntity = - new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION) - .setContent("InvalidContent") - .setPolicyVersion(0) - .build(); - assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) - .as("Validating empty JSON '{}' should throw InvalidPolicyException") - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Invalid policy"); - } - - @Test - public void testUnsupportedPolicyType() { - var policyEntity = - new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", METADATA_COMPACTION) - .setContent("InvalidContent") - .setPolicyVersion(0) - .build(); - - assertThatThrownBy(() -> PolicyValidators.validate(policyEntity)) - .isInstanceOf(InvalidPolicyException.class) - .hasMessageContaining("Unsupported policy type"); - } - - @Test - public void testValidPolicy() { - var policyEntity = - new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", DATA_COMPACTION) - .setContent("{\"enable\": false}") - .setPolicyVersion(0) - .build(); - PolicyValidators.validate(policyEntity); - } -}