diff --git a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java index ba28cc30356..f928980f4bd 100644 --- a/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java +++ b/persistence/nosql/persistence/metastore/src/main/java/org/apache/polaris/persistence/nosql/metastore/NoSqlMetaStore.java @@ -663,37 +663,75 @@ Optional hasOverlappingSiblings(long catalogId, String checkLocation) { var locationIdentifier = identifierFromLocationString(checkLocation); var locationIndexKey = locationIdentifier.toIndexKey(); - // TODO VALIDATE THE CHECKS HERE ! + + // Check for children and exact matches first using forward iteration (preserves + // existing test expectations on which conflicting location is reported). + // Also iterate fully in case early entries are filtered out. var iter = locationsIndex.iterator(locationIndexKey, null, false); - if (!iter.hasNext()) { - return Optional.empty(); + while (iter.hasNext()) { + var elem = iter.next(); + var elemKey = elem.key(); + var elemIdentifier = indexKeyToIdentifier(elemKey); + if (!elemIdentifier.startsWith(locationIdentifier)) { + break; // No more matches due to ordering + } + + var conflicting = + elem.value().entityIds().stream() + .map(IndexKey::key) + .map(byId::get) + .filter(Objects::nonNull) + .map(byName::get) + .filter(Objects::nonNull) + .map(objRef -> persistence.fetch(objRef, ContentObj.class)) + .filter(Objects::nonNull) + .map( + contentObj -> { + var conflictingBaseLocation = + contentObj.properties().get(ENTITY_BASE_LOCATION); + return conflictingBaseLocation != null + ? conflictingBaseLocation + : String.join("/", elemIdentifier.elements()); + }) + .findFirst(); + if (conflicting.isPresent()) { + return conflicting; + } } - var elem = iter.next(); - var elemKey = elem.key(); - var elemIdentifier = indexKeyToIdentifier(elemKey); - if (!elemIdentifier.startsWith(locationIdentifier)) { - return Optional.empty(); + // Check for parent (prefix) overlaps. These have shorter keys and are missed by + // forward iteration starting at the full target key. + for (int i = 1; i <= locationIdentifier.length(); i++) { + var prefixElements = locationIdentifier.elements().subList(0, i); + var prefix = ContentIdentifier.identifier(prefixElements); + var prefixKey = prefix.toIndexKey(); + var entry = locationsIndex.get(prefixKey); + if (entry != null) { + var conflicting = + entry.entityIds().stream() + .map(IndexKey::key) + .map(byId::get) + .filter(Objects::nonNull) + .map(byName::get) + .filter(Objects::nonNull) + .map(objRef -> persistence.fetch(objRef, ContentObj.class)) + .filter(Objects::nonNull) + .map( + contentObj -> { + var conflictingBaseLocation = + contentObj.properties().get(ENTITY_BASE_LOCATION); + return conflictingBaseLocation != null + ? conflictingBaseLocation + : String.join("/", prefix.elements()); + }) + .findFirst(); + if (conflicting.isPresent()) { + return conflicting; + } + } } - return elem.value().entityIds().stream() - .map(IndexKey::key) - .map(byId::get) - .filter(Objects::nonNull) - .map(byName::get) - .filter(Objects::nonNull) - .map(objRef -> persistence.fetch(objRef, ContentObj.class)) - .filter(Objects::nonNull) - .map( - contentObj -> { - // Check if conflict is the parent namespace - TODO recurse?? - var conflictingBaseLocation = - contentObj.properties().get(ENTITY_BASE_LOCATION); - return conflictingBaseLocation != null - ? conflictingBaseLocation - : String.join("/", elemIdentifier.elements()); - }) - .findFirst(); + return Optional.empty(); }); } diff --git a/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java b/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java index 98d3fb40aa6..7f617dd3238 100644 --- a/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java +++ b/persistence/nosql/persistence/metastore/src/test/java/org/apache/polaris/persistence/nosql/metastore/TestNoSqlMetaStoreManager.java @@ -164,6 +164,19 @@ public void overlappingLocations() { "ns2", Map.of(ENTITY_BASE_LOCATION, "s3://bucket/foo/")); assertThat(nsFoo).extracting(EntityResult::isSuccess, BOOLEAN).isTrue(); + + // Test parent namespace overlap detection for a new child location. + // This case was not detected by the original NoSQL implementation. + soft.assertThat( + metaStore.hasOverlappingSiblings( + callContext, + new NamespaceEntity.Builder(Namespace.of("x")) + .setCatalogId(catalog.getId()) + .setBaseLocation("s3://bucket/foo/newchild/") + .build())) + .isPresent() + .contains(Optional.of("s3://bucket/foo/")); + var nsBar = createEntity( List.of(catalog), @@ -242,6 +255,10 @@ public void overlappingLocations() { // Drop one of the entities with the duplicate base location metaStore.dropEntityIfExists(callContext, List.of(catalog), nsFoobar2.getEntity(), null, false); + + // Drop the parent too so the final "no overlap" check for a bar location is clean. + metaStore.dropEntityIfExists(callContext, List.of(catalog), nsFoo.getEntity(), null, false); + // No more overlaps soft.assertThat( metaStore.hasOverlappingSiblings( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogOverlapTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogOverlapTest.java new file mode 100644 index 00000000000..9b7503b325b --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractLocalIcebergCatalogOverlapTest.java @@ -0,0 +1,297 @@ +/* + * 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.service.catalog.iceberg; + +import static org.apache.polaris.service.admin.PolarisAuthzTestBase.SCHEMA; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.quarkus.test.junit.QuarkusMock; +import io.smallrye.common.annotation.Identifier; +import jakarta.inject.Inject; +import java.io.IOException; +import java.lang.reflect.Method; +import java.time.Clock; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.iceberg.CatalogProperties; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.ForbiddenException; +import org.apache.iceberg.inmemory.InMemoryFileIO; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; +import org.apache.polaris.core.admin.model.CreateCatalogRequest; +import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.config.FeatureConfiguration; +import org.apache.polaris.core.config.RealmConfig; +import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.entity.CatalogEntity; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; +import org.apache.polaris.core.persistence.MetaStoreManagerFactory; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.bootstrap.RootCredentialsSet; +import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; +import org.apache.polaris.core.persistence.resolver.ResolverFactory; +import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; +import org.apache.polaris.core.storage.aws.AwsCredentialsStorageIntegration; +import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo; +import org.apache.polaris.core.storage.cache.StorageCredentialCache; +import org.apache.polaris.service.admin.PolarisAdminService; +import org.apache.polaris.service.catalog.PolarisPassthroughResolutionView; +import org.apache.polaris.service.catalog.io.FileIOFactory; +import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider; +import org.apache.polaris.service.context.catalog.RealmContextHolder; +import org.apache.polaris.service.events.PolarisEventDispatcher; +import org.apache.polaris.service.events.PolarisEventMetadataFactory; +import org.apache.polaris.service.events.listeners.PolarisEventListener; +import org.apache.polaris.service.events.listeners.TestPolarisEventListener; +import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; +import org.apache.polaris.service.task.TaskExecutor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.mockito.Mockito; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +/** + * Base class for persistence-backend-agnostic overlap tests. Concrete subclasses provide a Quarkus + * test profile with location-overlap enforcement and the optimized sibling check enabled, so the + * tests exercise the {@code hasOverlappingSiblings} path of whichever metastore implementation the + * profile wires in. + */ +public abstract class AbstractLocalIcebergCatalogOverlapTest { + + protected static final String CATALOG_NAME = "polaris-catalog"; + protected static final String STORAGE_LOCATION = "s3://my-bucket/path/to/data"; + private static final String TEST_ACCESS_KEY = "test_access_key"; + private static final String SECRET_ACCESS_KEY = "secret_access_key"; + private static final String SESSION_TOKEN = "session_token"; + + @Inject Clock clock; + @Inject MetaStoreManagerFactory metaStoreManagerFactory; + @Inject StorageCredentialCache storageCredentialCache; + @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; + @Inject ServiceIdentityProvider serviceIdentityProvider; + @Inject PolarisDiagnostics diagServices; + + @Inject + @Identifier("test") + PolarisEventListener polarisEventListener; + + @Inject PolarisEventMetadataFactory eventMetadataFactory; + @Inject PolarisMetaStoreManager metaStoreManager; + @Inject CallContext callContext; + @Inject RealmConfig realmConfig; + @Inject RealmContextHolder realmContextHolder; + @Inject ResolutionManifestFactory resolutionManifestFactory; + @Inject StorageAccessConfigProvider storageAccessConfigProvider; + @Inject FileIOFactory fileIOFactory; + @Inject PolarisPrincipal authenticatedRoot; + @Inject PolarisAdminService adminService; + @Inject ResolverFactory resolverFactory; + @Inject PolarisEventDispatcher polarisEventDispatcher; + + private LocalIcebergCatalog catalog; + private String realmName; + private PolarisCallContext polarisContext; + private InMemoryFileIO fileIO; + private PolarisEntity catalogEntity; + + @BeforeAll + public static void setUpMocks() { + PolarisStorageIntegrationProviderImpl mock = + Mockito.mock(PolarisStorageIntegrationProviderImpl.class); + QuarkusMock.installMockForType(mock, PolarisStorageIntegrationProviderImpl.class); + } + + @BeforeEach + @SuppressWarnings("unchecked") + public void before(TestInfo testInfo) { + storageCredentialCache.invalidateAll(); + + realmName = + "realm_%s_%s" + .formatted( + testInfo.getTestMethod().map(Method::getName).orElse("test"), System.nanoTime()); + metaStoreManagerFactory + .bootstrapRealms( + List.of(realmName), + RootCredentialsSet.fromList(List.of(realmName + ",aClientId,aSecret"))) + .get(realmName); + + realmContextHolder.set(() -> realmName); + polarisContext = callContext.getPolarisCallContext(); + + AwsStorageConfigInfo storageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/jdoe") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(STORAGE_LOCATION, "s3://externally-owned-bucket")) + .build(); + catalogEntity = + adminService.createCatalog( + new CreateCatalogRequest( + new CatalogEntity.Builder() + .setName(CATALOG_NAME) + .setDefaultBaseLocation(STORAGE_LOCATION) + .addProperty( + FeatureConfiguration.ALLOW_EXTERNAL_TABLE_LOCATION.catalogConfig(), "true") + .addProperty( + FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), + "true") + .addProperty( + FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") + .setStorageConfigurationInfo(realmConfig, storageConfigModel, STORAGE_LOCATION) + .build() + .asCatalog(serviceIdentityProvider))); + + StsClient stsClient = Mockito.mock(StsClient.class); + when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class))) + .thenReturn( + AssumeRoleResponse.builder() + .credentials( + Credentials.builder() + .accessKeyId(TEST_ACCESS_KEY) + .secretAccessKey(SECRET_ACCESS_KEY) + .sessionToken(SESSION_TOKEN) + .build()) + .build()); + AwsStorageConfigurationInfo mockAwsConfig = + AwsStorageConfigurationInfo.builder() + .roleARN("arn:aws:iam::012345678901:role/mock") + .build(); + AwsCredentialsStorageIntegration storageIntegration = + new AwsCredentialsStorageIntegration( + (destination) -> stsClient, + config -> Optional.empty(), + storageCredentialCache, + mockAwsConfig, + callContext.getRealmConfig()); + when(storageIntegrationProvider.getStorageIntegration(Mockito.anyList())) + .thenReturn(storageIntegration); + + this.catalog = initCatalog("my-catalog", ImmutableMap.of()); + ((TestPolarisEventListener) polarisEventListener).clear(); + } + + @AfterEach + public void after() throws IOException { + catalog.close(); + metaStoreManager.purge(polarisContext); + } + + protected LocalIcebergCatalog catalog() { + return catalog; + } + + protected LocalIcebergCatalog initCatalog( + String catalogName, Map additionalProperties) { + LocalIcebergCatalog icebergCatalog = newIcebergCatalog(CATALOG_NAME); + fileIO = new InMemoryFileIO(); + icebergCatalog.setCatalogFileIo(fileIO); + ImmutableMap.Builder propertiesBuilder = + ImmutableMap.builder() + .put(CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO") + .putAll(additionalProperties); + icebergCatalog.initialize(CATALOG_NAME, propertiesBuilder.buildKeepingLast()); + return icebergCatalog; + } + + protected LocalIcebergCatalog newIcebergCatalog(String catalogName) { + return newIcebergCatalog(catalogName, metaStoreManager); + } + + protected LocalIcebergCatalog newIcebergCatalog( + String catalogName, PolarisMetaStoreManager metaStoreManager) { + return newIcebergCatalog(catalogName, metaStoreManager, fileIOFactory); + } + + protected LocalIcebergCatalog newIcebergCatalog( + String catalogName, PolarisMetaStoreManager metaStoreManager, FileIOFactory fileIOFactory) { + PolarisPassthroughResolutionView passthroughView = + new PolarisPassthroughResolutionView( + resolutionManifestFactory, authenticatedRoot, catalogName); + TaskExecutor taskExecutor = Mockito.mock(TaskExecutor.class); + return new LocalIcebergCatalog( + diagServices, + resolverFactory, + metaStoreManager, + polarisContext, + passthroughView, + authenticatedRoot, + taskExecutor, + storageAccessConfigProvider, + fileIOFactory, + polarisEventDispatcher, + eventMetadataFactory); + } + + @Test + public void testParentChildLocationOverlapWithOptimizedSiblingCheck() { + Namespace ns = Namespace.of("ns-for-overlap-test"); + catalog().createNamespace(ns); + + // Create a table at a nested location first. + TableIdentifier childTable = TableIdentifier.of(ns, "child-table"); + String childLoc = STORAGE_LOCATION + "/overlap-parent/child"; + catalog().buildTable(childTable, SCHEMA).withLocation(childLoc).create(); + + // Creating a second table at the parent (prefix) location must be rejected. This is the + // regression case for the NoSQL hasOverlappingSiblings fix: parent-prefix overlaps have + // shorter keys and were missed by the forward-only location-index scan. + TableIdentifier parentTable = TableIdentifier.of(ns, "parent-table"); + String parentLoc = STORAGE_LOCATION + "/overlap-parent"; + assertThatThrownBy( + () -> catalog().buildTable(parentTable, SCHEMA).withLocation(parentLoc).create()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Unable to create entity at location") + .hasMessageContaining("conflicts with existing table or namespace"); + + // Conversely, creating a child table after the parent table exists must also be rejected + // (covered by the forward location-index scan). + TableIdentifier parentTable2 = TableIdentifier.of(ns, "parent-table-2"); + String parentLoc2 = STORAGE_LOCATION + "/overlap-parent-2"; + catalog().buildTable(parentTable2, SCHEMA).withLocation(parentLoc2).create(); + + TableIdentifier anotherChildTable = TableIdentifier.of(ns, "another-child-table"); + String anotherChildLoc = STORAGE_LOCATION + "/overlap-parent-2/child"; + assertThatThrownBy( + () -> + catalog() + .buildTable(anotherChildTable, SCHEMA) + .withLocation(anotherChildLoc) + .create()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining("Unable to create entity at location") + .hasMessageContaining("conflicts with existing table or namespace"); + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergOverlappingTableTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergOverlappingTableTest.java index 7f6eb0e54a4..22990dce4d7 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergOverlappingTableTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergOverlappingTableTest.java @@ -674,4 +674,57 @@ void testStagedCreateRejectsCustomLocationWhenStructured(@TempDir Path tempDir) assertThat(createTableStaged(services, someLocation)) .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); } + + @Test + @DisplayName( + "Parent/child namespace and table overlaps are rejected when OPTIMIZED_SIBLING_CHECK is enabled") + void testParentChildOverlapWithOptimizedSiblingCheck(@TempDir Path tempDir) { + Map servicesWithOptimized = + Map.of( + "ALLOW_UNSTRUCTURED_TABLE_LOCATION", + "true", + "ALLOW_TABLE_LOCATION_OVERLAP", + "false", + "ALLOW_INSECURE_STORAGE_TYPES", + "true", + "SUPPORTED_CATALOG_STORAGE_TYPES", + List.of("FILE", "S3"), + OPTIMIZED_SIBLING_CHECK.key(), + "true"); + + // Use similar catalog config as other flag-enabled tests (hashed) to allow creation. + Map catalogConfig = + Map.of( + DEFAULT_LOCATION_OBJECT_STORAGE_PREFIX_ENABLED.catalogConfig(), "true", + ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), "true"); + + TestServices services = TestServices.builder().config(servicesWithOptimized).build(); + + String baseLocation = tempDir.toAbsolutePath().toUri().toString(); + if (baseLocation.endsWith("/")) { + baseLocation = baseLocation.substring(0, baseLocation.length() - 1); + } + createCatalogAndNamespace(services, catalogConfig, baseLocation); + + // Test that you cannot create a table at the namespace's own location (parent case) + String nsLocation = String.format("%s/%s/%s", baseLocation, catalog, namespace); + assertThat(createTable(services, nsLocation)) + .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); + + // Create a table using no explicit location (lets server derive a proper one under the ns) + String derivedLocation = createTableWithName(services, getTableName()); + assertThat(derivedLocation).isNotNull(); + + // Repeat of the derived location should be forbidden + assertThat(createTable(services, derivedLocation)) + .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); + + // Parent of the derived table location (the ns location) should be forbidden + assertThat(createTable(services, nsLocation)) + .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); + + // Child of the derived table location should be forbidden + assertThat(createTable(services, derivedLocation + "/child")) + .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); + } } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogNoSqlOverlapTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogNoSqlOverlapTest.java new file mode 100644 index 00000000000..8bb3aa5bb5b --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogNoSqlOverlapTest.java @@ -0,0 +1,52 @@ +/* + * 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.service.catalog.iceberg; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import java.util.HashMap; +import java.util.Map; +import org.apache.polaris.service.Profiles; + +/** + * NoSQL-backed overlap test. Inherits the assertions from {@link + * AbstractLocalIcebergCatalogOverlapTest} and wires in the NoSQL in-memory metastore with overlap + * enforcement enabled. + */ +@QuarkusTest +@TestProfile(LocalIcebergCatalogNoSqlOverlapTest.NoSqlOptimizedSiblingCheckProfile.class) +public class LocalIcebergCatalogNoSqlOverlapTest extends AbstractLocalIcebergCatalogOverlapTest { + + /** + * Profile that runs the NoSQL in-memory backend with location-overlap enforcement and the + * optimized sibling check enabled, so the tests exercise the NoSQL {@code hasOverlappingSiblings} + * path. + */ + public static class NoSqlOptimizedSiblingCheckProfile + extends Profiles.NoSqlIcebergCatalogProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(super.getConfigOverrides()); + overrides.put("polaris.features.\"ALLOW_TABLE_LOCATION_OVERLAP\"", "false"); + overrides.put("polaris.features.\"OPTIMIZED_SIBLING_CHECK\"", "true"); + overrides.put("polaris.features.\"ALLOW_OPTIMIZED_SIBLING_CHECK\"", "true"); + return overrides; + } + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogOverlapTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogOverlapTest.java new file mode 100644 index 00000000000..a66101a450b --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogOverlapTest.java @@ -0,0 +1,50 @@ +/* + * 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.service.catalog.iceberg; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import java.util.HashMap; +import java.util.Map; +import org.apache.polaris.service.Profiles; + +/** + * Default-persistence overlap test. Inherits the assertions from {@link + * AbstractLocalIcebergCatalogOverlapTest} and runs them against the default in-memory transactional + * metastore with overlap enforcement enabled. + */ +@QuarkusTest +@TestProfile(LocalIcebergCatalogOverlapTest.DefaultOverlapProfile.class) +public class LocalIcebergCatalogOverlapTest extends AbstractLocalIcebergCatalogOverlapTest { + + /** + * Profile that runs the default in-memory transactional backend with location-overlap enforcement + * and the optimized sibling check enabled. + */ + public static class DefaultOverlapProfile extends Profiles.DefaultIcebergCatalogProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(super.getConfigOverrides()); + overrides.put("polaris.features.\"ALLOW_TABLE_LOCATION_OVERLAP\"", "false"); + overrides.put("polaris.features.\"OPTIMIZED_SIBLING_CHECK\"", "true"); + overrides.put("polaris.features.\"ALLOW_OPTIMIZED_SIBLING_CHECK\"", "true"); + return overrides; + } + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogRelationalOverlapTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogRelationalOverlapTest.java new file mode 100644 index 00000000000..946174d9c46 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogRelationalOverlapTest.java @@ -0,0 +1,57 @@ +/* + * 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.service.catalog.iceberg; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import java.util.HashMap; +import java.util.Map; +import org.apache.polaris.service.Profiles; + +/** + * JDBC/relational-backed overlap test. Inherits the assertions from {@link + * AbstractLocalIcebergCatalogOverlapTest} and runs them against the relational-jdbc metastore (H2) + * with overlap enforcement enabled. + */ +@QuarkusTest +@TestProfile(LocalIcebergCatalogRelationalOverlapTest.RelationalOverlapProfile.class) +public class LocalIcebergCatalogRelationalOverlapTest + extends AbstractLocalIcebergCatalogOverlapTest { + + /** + * Profile that runs the relational-jdbc backend (H2 in-memory) with location-overlap enforcement + * and the optimized sibling check enabled. + */ + public static class RelationalOverlapProfile extends Profiles.DefaultIcebergCatalogProfile { + @Override + public Map getConfigOverrides() { + Map overrides = new HashMap<>(super.getConfigOverrides()); + overrides.put("polaris.features.\"ALLOW_TABLE_LOCATION_OVERLAP\"", "false"); + overrides.put("polaris.features.\"OPTIMIZED_SIBLING_CHECK\"", "true"); + overrides.put("polaris.features.\"ALLOW_OPTIMIZED_SIBLING_CHECK\"", "true"); + overrides.put("polaris.persistence.type", "relational-jdbc"); + overrides.put("polaris.persistence.auto-bootstrap-types", "relational-jdbc"); + overrides.put("quarkus.datasource.db-kind", "h2"); + overrides.put( + "quarkus.datasource.jdbc.url", + "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"); + return overrides; + } + } +}