diff --git a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java index bbc917bc0b8..4ca5b5ade27 100644 --- a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java +++ b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java @@ -46,6 +46,7 @@ import org.apache.polaris.core.entity.PolarisEntityId; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.entity.PolarisEntityUtils; import org.apache.polaris.core.entity.PolarisEvent; import org.apache.polaris.core.entity.PolarisGrantRecord; import org.apache.polaris.core.entity.PolarisPrincipalSecrets; @@ -810,11 +811,20 @@ Optional> hasOverlappingSiblings( if (!results.isEmpty()) { StorageLocation entityLocation = StorageLocation.of(entity.getBaseLocation()); for (PolarisBaseEntity result : results) { - StorageLocation potentialSiblingLocation = - StorageLocation.of(((LocationBasedEntity) result).getBaseLocation()); - if (entityLocation.isChildOf(potentialSiblingLocation) - || potentialSiblingLocation.isChildOf(entityLocation)) { - return Optional.of(Optional.of(potentialSiblingLocation.toString())); + // JDBC materializes persisted rows as PolarisBaseEntity. Resolve the sibling location + // via PolarisEntityUtils instead of casting to LocationBasedEntity. + Optional overlappingSiblingLocation = + PolarisEntityUtils.asLocationBasedEntity(PolarisEntity.of(result)) + .map(LocationBasedEntity::getBaseLocation) + .filter(location -> location != null && !location.isBlank()) + .map(StorageLocation::of) + .filter( + potentialSiblingLocation -> + entityLocation.isChildOf(potentialSiblingLocation) + || potentialSiblingLocation.isChildOf(entityLocation)) + .map(StorageLocation::toString); + if (overlappingSiblingLocation.isPresent()) { + return Optional.of(overlappingSiblingLocation); } } } diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java index ec1bc596911..54199d286b0 100644 --- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java +++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java @@ -22,16 +22,27 @@ import java.io.InputStream; import java.sql.SQLException; +import java.util.List; +import java.util.Map; import java.util.Optional; import javax.sql.DataSource; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisDefaultDiagServiceImpl; import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.entity.LocationBasedEntity; +import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityConstants; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.AtomicOperationMetaStoreManager; import org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest; import org.apache.polaris.core.persistence.PolarisTestMetaStoreManager; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.Assumptions; import org.h2.jdbcx.JdbcConnectionPool; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; public abstract class AtomicMetastoreManagerWithJdbcBasePersistenceImplTest @@ -79,6 +90,82 @@ protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() { return new PolarisTestMetaStoreManager(metaStoreManager, callCtx); } + @Test + void testHasOverlappingSiblingsUsesStoredBaseLocation() { + // The optimized check relies on the location_without_scheme column added in schema v2. + Assumptions.assumeThat(schemaVersion()).isGreaterThanOrEqualTo(2); + + var metaStoreManager = polarisTestMetaStoreManager.polarisMetaStoreManager(); + var callContext = polarisTestMetaStoreManager.polarisCallContext(); + PolarisBaseEntity catalog = + new PolarisBaseEntity( + PolarisEntityConstants.getNullId(), + metaStoreManager.generateNewEntityId(callContext).getId(), + PolarisEntityType.CATALOG, + PolarisEntitySubType.NULL_SUBTYPE, + PolarisEntityConstants.getRootEntityId(), + "overlap_test_catalog"); + catalog = metaStoreManager.createCatalog(callContext, catalog, List.of()).getCatalog(); + + PolarisBaseEntity existingNamespace = + buildLocationBasedNamespace( + catalog, + metaStoreManager.generateNewEntityId(callContext).getId(), + "existing", + "s3://bucket/warehouse/existing/"); + metaStoreManager.createEntityIfNotExists( + callContext, List.of(PolarisEntity.toCore(catalog)), existingNamespace); + + TestLocationBasedEntity candidateNamespace = + new TestLocationBasedEntity( + buildLocationBasedNamespace( + catalog, + metaStoreManager.generateNewEntityId(callContext).getId(), + "candidate", + "s3://bucket/warehouse/existing/child")); + + Assertions.assertThat(metaStoreManager.hasOverlappingSiblings(callContext, candidateNamespace)) + .contains(Optional.of("s3://bucket/warehouse/existing/")); + + TestLocationBasedEntity nonOverlappingNamespace = + new TestLocationBasedEntity( + buildLocationBasedNamespace( + catalog, + metaStoreManager.generateNewEntityId(callContext).getId(), + "non_overlapping", + "s3://bucket/warehouse/non-overlapping")); + + Assertions.assertThat( + metaStoreManager.hasOverlappingSiblings(callContext, nonOverlappingNamespace)) + .contains(Optional.empty()); + } + + private static PolarisBaseEntity buildLocationBasedNamespace( + PolarisBaseEntity catalog, long entityId, String name, String baseLocation) { + return new PolarisBaseEntity.Builder() + .catalogId(catalog.getId()) + .parentId(catalog.getId()) + .id(entityId) + .typeCode(PolarisEntityType.NAMESPACE.getCode()) + .subTypeCode(PolarisEntitySubType.NULL_SUBTYPE.getCode()) + .name(name) + .propertiesAsMap(Map.of(PolarisEntityConstants.ENTITY_BASE_LOCATION, baseLocation)) + .internalPropertiesAsMap(Map.of()) + .build(); + } + + private static class TestLocationBasedEntity extends PolarisEntity + implements LocationBasedEntity { + TestLocationBasedEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + @Override + public String getBaseLocation() { + return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION); + } + } + private static class H2JdbcConfiguration implements RelationalJdbcConfiguration { @Override