You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by di...@apache.org on 2026/05/20 02:27:32 UTC
(polaris) branch main updated: Make location overlap checks more robust (#4408)
This is an automated email from the ASF dual-hosted git repository.
dimas-b pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new 4cc8663900 Make location overlap checks more robust (#4408)
4cc8663900 is described below
commit 4cc8663900026192adddb76fd16255c761339e71
Author: Dmitri Bourlatchkov <dm...@gmail.com>
AuthorDate: Tue May 19 22:27:27 2026 -0400
Make location overlap checks more robust (#4408)
Feed sibling paths to the `Resolver` as optional and ignore non-found cases.
Note: it is normal for an entity to be deleted during the sibling resolution process. Ignoring unresolved entities is to compensate for data changes between the "list" operation and the "resolveAll" operation.
Throw a `CommitConflictException` if coherent data about all siblings cannot be resolved.
Subsequently, sibling resolution failures lead to 409 (Conflict) errors at the REST API level instead of 500 (Server Failure) now.
Fixes #4407
---
.../polaris/core/entity/PolarisEntityUtils.java | 19 +++
.../core/exceptions/CommitConflictException.java | 4 +
.../core/entity/PolarisEntityUtilsTest.java | 22 ++++
.../CatalogGenericTableEventServiceDelegator.java | 16 +++
.../service/catalog/iceberg/IcebergCatalog.java | 128 +++++++++++--------
.../admin/PolarisS3InteroperabilityTest.java | 2 +-
.../catalog/iceberg/IcebergCatalogTest.java | 142 +++++++++++++++++++++
.../iceberg/IcebergOverlappingTableTest.java | 32 +++++
.../org/apache/polaris/service/TestServices.java | 44 +++++++
9 files changed, 352 insertions(+), 57 deletions(-)
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityUtils.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityUtils.java
index f8c7c5440c..e9a2132c5d 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityUtils.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityUtils.java
@@ -25,8 +25,11 @@ import com.google.common.collect.Iterables;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
+import java.util.Optional;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.rest.RESTUtil;
+import org.apache.polaris.core.entity.table.GenericTableEntity;
+import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
/**
* Polaris-owned encoding/decoding for namespaces stored in entity internal properties.
@@ -75,4 +78,20 @@ public final class PolarisEntityUtils {
}
return Namespace.of(levels);
}
+
+ public static Optional<LocationBasedEntity> asLocationBasedEntity(PolarisEntity entity) {
+ return Optional.ofNullable(
+ switch (entity.getType()) {
+ case CATALOG -> CatalogEntity.of(entity);
+ case NAMESPACE -> NamespaceEntity.of(entity);
+ case TABLE_LIKE ->
+ switch (entity.getSubType()) {
+ case GENERIC_TABLE -> GenericTableEntity.of(entity);
+ case ICEBERG_TABLE, ICEBERG_VIEW -> IcebergTableLikeEntity.of(entity);
+ default -> null;
+ };
+
+ default -> null;
+ });
+ }
}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/exceptions/CommitConflictException.java b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/CommitConflictException.java
index 75a61204e2..a1e11cd003 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/exceptions/CommitConflictException.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/exceptions/CommitConflictException.java
@@ -22,6 +22,10 @@ package org.apache.polaris.core.exceptions;
import com.google.errorprone.annotations.FormatMethod;
public class CommitConflictException extends PolarisException {
+ public CommitConflictException(String message) {
+ super(message);
+ }
+
@FormatMethod
public CommitConflictException(String message, Object... args) {
super(String.format(message, args));
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisEntityUtilsTest.java b/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisEntityUtilsTest.java
index ccbf824db9..21400389f2 100644
--- a/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisEntityUtilsTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisEntityUtilsTest.java
@@ -21,6 +21,7 @@ package org.apache.polaris.core.entity;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.Map;
import java.util.stream.Stream;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.rest.RESTUtil;
@@ -118,4 +119,25 @@ public class PolarisEntityUtilsTest {
assertThat(PolarisEntityUtils.decodeNamespace(PolarisEntityUtils.encodeNamespace(ns)))
.isEqualTo(ns);
}
+
+ public static Stream<Arguments> asLocationBasedEntityCases() {
+ return Stream.of(
+ Arguments.of(PolarisEntityType.FILE, PolarisEntitySubType.ANY_SUBTYPE, true),
+ Arguments.of(PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ANY_SUBTYPE, true),
+ Arguments.of(PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.NULL_SUBTYPE, true),
+ Arguments.of(PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.GENERIC_TABLE, false),
+ Arguments.of(PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE, false),
+ Arguments.of(PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_VIEW, false),
+ Arguments.of(PolarisEntityType.CATALOG, PolarisEntitySubType.NULL_SUBTYPE, false),
+ Arguments.of(PolarisEntityType.NAMESPACE, PolarisEntitySubType.NULL_SUBTYPE, false));
+ }
+
+ @ParameterizedTest
+ @MethodSource("asLocationBasedEntityCases")
+ public void testAsLocationBasedEntity(
+ PolarisEntityType type, PolarisEntitySubType subType, boolean empty) {
+ PolarisEntity sample =
+ new PolarisEntity(0, type, subType, 1, 2, "name", 0, 0, 0, 0, Map.of(), Map.of(), 0, 1);
+ assertThat(PolarisEntityUtils.asLocationBasedEntity(sample).isEmpty()).isEqualTo(empty);
+ }
}
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/CatalogGenericTableEventServiceDelegator.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/CatalogGenericTableEventServiceDelegator.java
index 3d7701551f..79916d3935 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/CatalogGenericTableEventServiceDelegator.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/CatalogGenericTableEventServiceDelegator.java
@@ -19,6 +19,7 @@
package org.apache.polaris.service.catalog.generic;
+import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Priority;
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
@@ -48,6 +49,21 @@ public class CatalogGenericTableEventServiceDelegator
@Inject PolarisEventMetadataFactory eventMetadataFactory;
@Inject CatalogPrefixParser prefixParser;
+ // Default constructor for CDI
+ public CatalogGenericTableEventServiceDelegator() {}
+
+ @VisibleForTesting
+ public CatalogGenericTableEventServiceDelegator(
+ GenericTableCatalogAdapter delegate,
+ PolarisEventDispatcher polarisEventDispatcher,
+ PolarisEventMetadataFactory eventMetadataFactory,
+ CatalogPrefixParser prefixParser) {
+ this.delegate = delegate;
+ this.polarisEventDispatcher = polarisEventDispatcher;
+ this.eventMetadataFactory = eventMetadataFactory;
+ this.prefixParser = prefixParser;
+ }
+
@Override
public Response createGenericTable(
String prefix,
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java
index ae6b23e98a..e7e6009c40 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java
@@ -37,6 +37,7 @@ import java.io.Closeable;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -51,7 +52,6 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.apache.iceberg.BaseTable;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.CatalogUtil;
@@ -109,8 +109,8 @@ 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.entity.PolarisEntityUtils;
import org.apache.polaris.core.entity.PolarisTaskConstants;
-import org.apache.polaris.core.entity.table.GenericTableEntity;
import org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
import org.apache.polaris.core.exceptions.CommitConflictException;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
@@ -1242,70 +1242,86 @@ public class IcebergCatalog extends BaseMetastoreViewCatalog
return Namespace.of(newLevels);
})
.toList();
- LOGGER.debug(
- "Resolving {} sibling entities to validate location",
- siblingTables.size() + siblingNamespaces.size());
- PolarisResolutionManifest resolutionManifest =
- new PolarisResolutionManifest(
- diagnostics,
- callContext.getRealmContext(),
- resolverFactory,
- principal,
- parentPath.getFirst().getName());
+ List<ResolvedPathKey> pathsToResolve =
+ new ArrayList<>(siblingTables.size() + siblingNamespaces.size());
siblingTables.forEach(
- tbl ->
- resolutionManifest.addPath(
- new ResolverPath(
- PolarisCatalogHelpers.tableIdentifierToList(tbl),
- PolarisEntityType.TABLE_LIKE)));
+ tbl -> {
+ if (!tbl.name().equals(name)) {
+ pathsToResolve.add(ResolvedPathKey.ofTableLike(tbl));
+ }
+ });
siblingNamespaces.forEach(
- ns ->
- resolutionManifest.addPath(
- new ResolverPath(Arrays.asList(ns.levels()), PolarisEntityType.NAMESPACE)));
+ ns -> {
+ if (!ns.level(ns.length() - 1).equals(name)) {
+ pathsToResolve.add(ResolvedPathKey.ofNamespace(ns));
+ }
+ });
+
+ StorageLocation targetLocation = StorageLocation.of(location);
+ for (PolarisEntity entityToCheck :
+ resolveOptionalPaths(pathsToResolve, parentPath.getFirst().getName())) {
+ PolarisEntityUtils.asLocationBasedEntity(entityToCheck)
+ .map(LocationBasedEntity::getBaseLocation)
+ .map(StorageLocation::of)
+ .ifPresent(
+ siblingLocation -> {
+ if (targetLocation.isChildOf(siblingLocation)
+ || siblingLocation.isChildOf(targetLocation)) {
+ throw new ForbiddenException(
+ "Unable to create entity at location '%s' because it conflicts with existing table or namespace at "
+ + "location '%s'",
+ targetLocation, siblingLocation);
+ }
+ });
+ }
+ }
+
+ @VisibleForTesting
+ List<PolarisEntity> resolveOptionalPaths(List<ResolvedPathKey> keys, String catalogName) {
+ LOGGER.debug("Resolving {} sibling entities to validate location", keys.size());
+
+ PolarisResolutionManifest resolutionManifest =
+ new PolarisResolutionManifest(
+ diagnostics, callContext.getRealmContext(), resolverFactory, principal, catalogName);
+
+ keys.forEach(
+ k -> {
+ resolutionManifest.addPath(new ResolverPath(k, true)); // optional path
+ });
+
ResolverStatus status = resolutionManifest.resolveAll();
- if (!status.getStatus().equals(ResolverStatus.StatusEnum.SUCCESS)) {
+
+ if (status.getStatus() != ResolverStatus.StatusEnum.SUCCESS) {
String message =
"Unable to resolve sibling entities to validate location - " + status.getStatus();
if (status.getStatus().equals(ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED)) {
message += ". Could not resolve entity: " + status.getFailedToResolvedEntityName();
+ } else if (status
+ .getStatus()
+ .equals(ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED)) {
+ ResolverPath path = status.getFailedToResolvePath();
+ if (path != null) {
+ message += ". path: " + String.join(".", path.entityNames());
+ message += ", failed index: " + status.getFailedToResolvedEntityIndex();
+ }
}
- throw new IllegalStateException(message);
+
+ throw new CommitConflictException(message);
}
- StorageLocation targetLocation = StorageLocation.of(location);
- Stream.concat(
- siblingTables.stream()
- .filter(tbl -> !tbl.name().equals(name))
- .map(
- tbl -> {
- PolarisResolvedPathWrapper resolveTablePath =
- resolutionManifest.getResolvedPath(ResolvedPathKey.ofTableLike(tbl));
- PolarisEntity tableEntity = resolveTablePath.getRawLeafEntity();
- if (tableEntity.getSubType() == PolarisEntitySubType.GENERIC_TABLE) {
- return GenericTableEntity.of(tableEntity).getBaseLocation();
- }
- return IcebergTableLikeEntity.of(tableEntity).getBaseLocation();
- }),
- siblingNamespaces.stream()
- .filter(ns -> !ns.level(ns.length() - 1).equals(name))
- .map(
- ns -> {
- PolarisResolvedPathWrapper resolveNamespacePath =
- resolutionManifest.getResolvedPath(ResolvedPathKey.ofNamespace(ns));
- return NamespaceEntity.of(resolveNamespacePath.getRawLeafEntity())
- .getBaseLocation();
- }))
- .filter(java.util.Objects::nonNull)
- .map(StorageLocation::of)
- .forEach(
- siblingLocation -> {
- if (targetLocation.isChildOf(siblingLocation)
- || siblingLocation.isChildOf(targetLocation)) {
- throw new org.apache.iceberg.exceptions.ForbiddenException(
- "Unable to create table at location '%s' because it conflicts with existing table or namespace at location '%s'",
- targetLocation, siblingLocation);
- }
- });
+ List<PolarisEntity> result = new ArrayList<>(keys.size());
+ keys.forEach(
+ k -> {
+ PolarisResolvedPathWrapper path = resolutionManifest.getResolvedPath(k);
+ if (path != null) {
+ PolarisEntity entity = path.getRawLeafEntity();
+ if (entity != null) {
+ result.add(entity);
+ }
+ }
+ });
+
+ return result;
}
private class PolarisIcebergCatalogTableBuilder
diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisS3InteroperabilityTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisS3InteroperabilityTest.java
index d984234a68..60031bbc5d 100644
--- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisS3InteroperabilityTest.java
+++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisS3InteroperabilityTest.java
@@ -249,7 +249,7 @@ public class PolarisS3InteroperabilityTest {
"tbl2",
table2Scheme,
makeTableLocation(catalogName, "ns1", "tbl1", table2Scheme)));
- assertThat(ex.getMessage()).contains("Unable to create table at location");
+ assertThat(ex.getMessage()).contains("Unable to create entity at location");
assertThat(ex.getMessage()).contains(table1Scheme);
assertThat(ex.getMessage()).contains(table2Scheme);
}
diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogTest.java
new file mode 100644
index 0000000000..b57bc2b9e2
--- /dev/null
+++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.polaris.core.PolarisDiagnostics;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+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.entity.PolarisEntityType;
+import org.apache.polaris.core.exceptions.CommitConflictException;
+import org.apache.polaris.core.persistence.ResolvedPolarisEntity;
+import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifestCatalogView;
+import org.apache.polaris.core.persistence.resolver.ResolvedPathKey;
+import org.apache.polaris.core.persistence.resolver.Resolver;
+import org.apache.polaris.core.persistence.resolver.ResolverFactory;
+import org.apache.polaris.core.persistence.resolver.ResolverPath;
+import org.apache.polaris.core.persistence.resolver.ResolverStatus;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class IcebergCatalogTest {
+
+ @Mock ResolverFactory resolverFactory;
+ @Mock CallContext callContext;
+ @Mock PolarisResolutionManifestCatalogView resolvedEntityView;
+ @Mock CatalogEntity catalogEntity;
+ @Mock PolarisDiagnostics diagnostics;
+ @Mock Resolver resolver;
+
+ @Mock PolarisEntity ns1;
+ @Mock PolarisEntity ns2;
+ @Mock PolarisEntity table3;
+
+ private final PolarisPrincipal principal = PolarisPrincipal.of("test", Map.of(), Set.of());
+
+ private IcebergCatalog catalog;
+ private ResolvedPolarisEntity rns1 = new ResolvedPolarisEntity(ns1, List.of(), List.of());
+ private ResolvedPolarisEntity rns2 = new ResolvedPolarisEntity(ns2, List.of(), List.of());
+ private ResolvedPolarisEntity rt3 = new ResolvedPolarisEntity(table3, List.of(), List.of());
+
+ @BeforeEach
+ void initMocks() {
+ when(resolvedEntityView.getResolvedCatalogEntity()).thenReturn(catalogEntity);
+ when(resolverFactory.createResolver(any(), any())).thenReturn(resolver);
+ when(resolver.resolveAll()).thenReturn(new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS));
+
+ rns1 = new ResolvedPolarisEntity(ns1, List.of(), List.of());
+ rns2 = new ResolvedPolarisEntity(ns2, List.of(), List.of());
+ rt3 = new ResolvedPolarisEntity(table3, List.of(), List.of());
+
+ catalog =
+ new IcebergCatalog(
+ diagnostics,
+ resolverFactory,
+ null,
+ callContext,
+ resolvedEntityView,
+ principal,
+ null,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ @Test
+ void testFailedLocationSiblingResolutionException() {
+ when(resolver.resolveAll())
+ .thenReturn(new ResolverStatus(PolarisEntityType.TABLE_LIKE, "test-name"));
+ Assertions.assertThatThrownBy(() -> catalog.resolveOptionalPaths(List.of(), "test"))
+ .isInstanceOf(CommitConflictException.class)
+ .hasMessageContaining("Unable to resolve sibling entities to validate location")
+ .hasMessageContaining("test-name")
+ .hasMessageContaining(ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED.name());
+
+ when(resolver.resolveAll())
+ .thenReturn(
+ new ResolverStatus(
+ new ResolverPath(
+ List.of("ns1", "ns2", "table3"), PolarisEntityType.TABLE_LIKE, true),
+ 123));
+ Assertions.assertThatThrownBy(() -> catalog.resolveOptionalPaths(List.of(), "test"))
+ .isInstanceOf(CommitConflictException.class)
+ .hasMessageContaining("Unable to resolve sibling entities to validate location")
+ .hasMessageContaining("ns1.ns2.table3")
+ .hasMessageContaining("failed index: 123")
+ .hasMessageContaining(ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED.name());
+ }
+
+ @Test
+ void testLocationSiblingResolution() {
+ ResolvedPathKey k1 = ResolvedPathKey.ofTableLike(TableIdentifier.of("ns1", "ns2", "t3"));
+ ResolvedPathKey k2 = ResolvedPathKey.ofTableLike(TableIdentifier.of("missing"));
+ ResolvedPathKey k3 = ResolvedPathKey.ofNamespace(Namespace.of("ns1", "ns2"));
+ ResolvedPathKey k4 = ResolvedPathKey.ofNamespace(Namespace.of("ns1", "ns2", "partial"));
+
+ List<List<ResolvedPolarisEntity>> resolved = new ArrayList<>();
+ resolved.add(List.of(rns1, rns2, rt3));
+ resolved.add(List.of());
+ resolved.add(List.of(rns1, rns2));
+ resolved.add(List.of(rns1, rns2));
+
+ when(resolver.getResolvedPaths()).thenReturn(resolved);
+ List<PolarisEntity> entities = catalog.resolveOptionalPaths(List.of(k1, k2, k3, k4), "test");
+ assertThat(entities).hasSize(2);
+ assertThat(entities.get(0)).isSameAs(table3);
+ assertThat(entities.get(1)).isSameAs(ns2);
+ }
+}
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 12d613f2b3..da1792b4d0 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
@@ -45,6 +45,7 @@ import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
import org.apache.polaris.service.TestServices;
import org.apache.polaris.service.catalog.common.LocationUtils;
+import org.apache.polaris.service.types.CreateGenericTableRequest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -86,6 +87,28 @@ public class IcebergOverlappingTableTest {
}
}
+ private int createGenericTable(TestServices services, String location) {
+ CreateGenericTableRequest createTableRequest =
+ CreateGenericTableRequest.builder()
+ .setName(getTableName())
+ .setBaseLocation(location)
+ .build();
+ try (Response response =
+ services
+ .genericTableApi()
+ .createGenericTable(
+ catalog,
+ namespace,
+ createTableRequest,
+ null,
+ services.realmContext(),
+ services.securityContext())) {
+ return response.getStatus();
+ } catch (ForbiddenException e) {
+ return Response.Status.FORBIDDEN.getStatusCode();
+ }
+ }
+
/** Attempt to stage-create a table at a given location, and return the response code. */
private int createTableStaged(TestServices services, String location) {
CreateTableRequest createTableRequest =
@@ -246,6 +269,11 @@ public class IcebergOverlappingTableTest {
services, String.format("%s/%s/%s/table_1", baseLocation, catalog, namespace)))
.isEqualTo(Response.Status.OK.getStatusCode());
+ assertThat(
+ createGenericTable(
+ services, String.format("%s/%s/%s/generic_1", baseLocation, catalog, namespace)))
+ .isEqualTo(Response.Status.OK.getStatusCode());
+
// Unrelated path
assertThat(
createTable(
@@ -263,6 +291,10 @@ public class IcebergOverlappingTableTest {
createTable(
services, String.format("%s/%s/%s/table_100", baseLocation, catalog, namespace)))
.isEqualTo(expectedStatusForOverlaps);
+ assertThat(
+ createTable(
+ services, String.format("%s/%s/%s/generic_1", baseLocation, catalog, namespace)))
+ .isEqualTo(expectedStatusForOverlaps);
// Parent of existing location
assertThat(createTable(services, String.format("%s/%s/%s", baseLocation, catalog, namespace)))
diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index 3cac9befee..23fbadbf16 100644
--- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -72,6 +72,13 @@ import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi;
import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService;
import org.apache.polaris.service.catalog.api.IcebergRestConfigurationApi;
import org.apache.polaris.service.catalog.api.IcebergRestConfigurationApiService;
+import org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApi;
+import org.apache.polaris.service.catalog.api.PolarisCatalogGenericTableApiService;
+import org.apache.polaris.service.catalog.generic.CatalogGenericTableEventServiceDelegator;
+import org.apache.polaris.service.catalog.generic.GenericTableCatalogAdapter;
+import org.apache.polaris.service.catalog.generic.GenericTableCatalogHandler;
+import org.apache.polaris.service.catalog.generic.GenericTableCatalogHandlerFactory;
+import org.apache.polaris.service.catalog.generic.ImmutableGenericTableCatalogHandler;
import org.apache.polaris.service.catalog.iceberg.CatalogHandlerUtils;
import org.apache.polaris.service.catalog.iceberg.IcebergCatalogAdapter;
import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandler;
@@ -107,6 +114,7 @@ public record TestServices(
Clock clock,
PolarisCatalogsApi catalogsApi,
IcebergRestCatalogApi restApi,
+ PolarisCatalogGenericTableApi genericTableApi,
IcebergRestConfigurationApi restConfigurationApi,
IcebergCatalogAdapter catalogAdapter,
RealmConfigurationSource configurationSource,
@@ -379,6 +387,41 @@ public record TestServices(
IcebergRestConfigurationApi restConfigurationApi =
new IcebergRestConfigurationApi(finalRestConfigurationService);
+ GenericTableCatalogHandlerFactory genericHandlerFactory =
+ new GenericTableCatalogHandlerFactory() {
+ @Override
+ public GenericTableCatalogHandler createHandler(
+ String catalogName, PolarisPrincipal principal) {
+ return ImmutableGenericTableCatalogHandler.builder()
+ .catalogName(catalogName)
+ .polarisPrincipal(principal)
+ .callContext(callContext)
+ .resolutionManifestFactory(resolutionManifestFactory)
+ .metaStoreManager(metaStoreManager)
+ .authorizer(authorizer)
+ .credentialManager(credentialManager)
+ .federatedCatalogFactories(federatedCatalogFactory)
+ .build();
+ }
+ };
+ GenericTableCatalogAdapter genericTableCatalogAdapter =
+ new GenericTableCatalogAdapter(
+ callContext,
+ new DefaultCatalogPrefixParser(),
+ reservedProperties,
+ genericHandlerFactory);
+ PolarisCatalogGenericTableApiService genericTableService = genericTableCatalogAdapter;
+ if (useEventDelegator) {
+ genericTableService =
+ new CatalogGenericTableEventServiceDelegator(
+ genericTableCatalogAdapter,
+ polarisEventDispatcher,
+ eventMetadataFactory,
+ new DefaultCatalogPrefixParser());
+ }
+ PolarisCatalogGenericTableApi genericTableApi =
+ new PolarisCatalogGenericTableApi(genericTableService);
+
PolarisAdminService adminService =
new PolarisAdminService(
callContext,
@@ -398,6 +441,7 @@ public record TestServices(
clock,
catalogsApi,
restApi,
+ genericTableApi,
restConfigurationApi,
catalogService,
configurationSource,