This is an automated email from the ASF dual-hosted git repository.

emaynard 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 6c02252e WASB Support (#344)
6c02252e is described below

commit 6c02252e339cedd53d7ac64a87ce52b3bb4d27d2
Author: Eric Maynard <[email protected]>
AuthorDate: Tue Oct 8 13:40:27 2024 -0400

    WASB Support (#344)
    
    * multiple prefixes
    
    * stable tests
    
    * autolint
    
    * add todos
    
    * implement todos
    
    * autolint
    
    * more fixes
    
    * autolint
    
    * fix test
    
    * fix check
    
    * move tenant ID to env var
    
    * another fix
    
    * autolint
    
    * fix another location check (?)
    
    * autolint
    
    * clean up for review
    
    * polish
    
    * autolint
    
    * adjustments per review
    
    * autolint
---
 .../core/storage/FileStorageConfigurationInfo.java |  4 +-
 .../core/storage/InMemoryStorageIntegration.java   | 31 +++-----
 .../storage/PolarisStorageConfigurationInfo.java   | 19 +++--
 .../polaris/core/storage/StorageLocation.java      | 90 ++++++++++++++++++++++
 .../polaris/core/storage/azure/AzureLocation.java  | 51 ++++++++++--
 .../polaris/core/storage/StorageUtilTest.java      |  2 +-
 .../aws/AwsCredentialsStorageIntegrationTest.java  |  2 +-
 .../AzureCredentialStorageIntegrationTest.java     |  7 +-
 .../service/storage/azure/AzureLocationTest.java   | 25 +++++-
 .../polaris/service/admin/PolarisAdminService.java | 22 +++---
 .../service/catalog/BasePolarisCatalog.java        | 23 +++---
 .../polaris/service/entity/CatalogEntityTest.java  |  6 +-
 12 files changed, 212 insertions(+), 70 deletions(-)

diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
index e5e1a319..2847d5a8 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
@@ -20,6 +20,7 @@ package org.apache.polaris.core.storage;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
 import java.util.List;
+import java.util.Locale;
 import org.jetbrains.annotations.NotNull;
 
 /**
@@ -42,7 +43,8 @@ public class FileStorageConfigurationInfo extends 
PolarisStorageConfigurationInf
 
   @Override
   public void validatePrefixForStorageType(String loc) {
-    if (!loc.startsWith(getStorageType().getPrefix())
+    if (getStorageType().getPrefixes().stream()
+            .noneMatch(p -> loc.toLowerCase(Locale.ROOT).startsWith(p))
         && !loc.startsWith("file:/")
         && !loc.startsWith("/")
         && !loc.equals("*")) {
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/InMemoryStorageIntegration.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/InMemoryStorageIntegration.java
index 4197760f..ce130668 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/InMemoryStorageIntegration.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/InMemoryStorageIntegration.java
@@ -19,11 +19,10 @@
 package org.apache.polaris.core.storage;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.NavigableSet;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.polaris.core.context.CallContext;
@@ -62,8 +61,7 @@ public abstract class InMemoryStorageIntegration<T extends 
PolarisStorageConfigu
           @NotNull Set<String> locations) {
     // trim trailing / from allowed locations so that locations missing the 
trailing slash still
     // match
-    // TODO: Canonicalize with URI and compare scheme/authority/path 
components separately
-    TreeSet<String> allowedLocations =
+    Set<String> allowedLocationStrings =
         storageConfig.getAllowedLocations().stream()
             .map(
                 str -> {
@@ -74,7 +72,10 @@ public abstract class InMemoryStorageIntegration<T extends 
PolarisStorageConfigu
                   }
                 })
             .map(str -> str.replace("file:///", "file:/"))
-            .collect(Collectors.toCollection(TreeSet::new));
+            .collect(Collectors.toSet());
+    List<StorageLocation> allowedLocations =
+        
allowedLocationStrings.stream().map(StorageLocation::of).collect(Collectors.toList());
+
     boolean allowWildcardLocation =
         Optional.ofNullable(CallContext.getCurrentContext())
             .flatMap(c -> Optional.ofNullable(c.getPolarisCallContext()))
@@ -84,7 +85,7 @@ public abstract class InMemoryStorageIntegration<T extends 
PolarisStorageConfigu
                         .getConfiguration(pc, "ALLOW_WILDCARD_LOCATION", 
false))
             .orElse(false);
 
-    if (allowWildcardLocation && allowedLocations.contains("*")) {
+    if (allowWildcardLocation && allowedLocationStrings.contains("*")) {
       return locations.stream()
           .collect(
               Collectors.toMap(
@@ -100,21 +101,9 @@ public abstract class InMemoryStorageIntegration<T extends 
PolarisStorageConfigu
     }
     Map<String, Map<PolarisStorageActions, ValidationResult>> resultMap = new 
HashMap<>();
     for (String rawLocation : locations) {
-      String location = rawLocation.replace("file:///", "file:/");
-      StringBuilder builder = new StringBuilder();
-      NavigableSet<String> prefixes = allowedLocations;
-      boolean validLocation = false;
-      for (char c : location.toCharArray()) {
-        builder.append(c);
-        prefixes = allowedLocations.tailSet(builder.toString(), true);
-        if (prefixes.isEmpty()) {
-          break;
-        } else if (prefixes.first().equals(builder.toString())) {
-          validLocation = true;
-          break;
-        }
-      }
-      final boolean isValidLocation = validLocation;
+      StorageLocation storageLocation = StorageLocation.of(rawLocation);
+      final boolean isValidLocation =
+          allowedLocations.stream().anyMatch(storageLocation::isChildOf);
       Map<PolarisStorageActions, ValidationResult> locationResult =
           actions.stream()
               .collect(
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
index 0239a9fe..4b284926 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
@@ -222,10 +222,11 @@ public abstract class PolarisStorageConfigurationInfo {
 
   /** Validate if the provided allowed locations are valid for the storage 
type */
   protected void validatePrefixForStorageType(String loc) {
-    if (!loc.toLowerCase(Locale.ROOT).startsWith(storageType.prefix)) {
+    if (storageType.prefixes.stream().noneMatch(p -> 
loc.toLowerCase(Locale.ROOT).startsWith(p))) {
       throw new IllegalArgumentException(
           String.format(
-              "Location prefix not allowed: '%s', expected prefix: '%s'", loc, 
storageType.prefix));
+              "Location prefix not allowed: '%s', expected prefixes: '%s'",
+              loc, String.join(",", storageType.prefixes)));
     }
   }
 
@@ -240,19 +241,23 @@ public abstract class PolarisStorageConfigurationInfo {
   /** Polaris' storage type, each has a fixed prefix for its location */
   public enum StorageType {
     S3("s3://"),
-    AZURE("abfs"), // abfs or abfss
+    AZURE(List.of("abfs://", "wasb://", "abfss://", "wasbs://")),
     GCS("gs://"),
     FILE("file://"),
     ;
 
-    final String prefix;
+    private final List<String> prefixes;
 
     StorageType(String prefix) {
-      this.prefix = prefix;
+      this.prefixes = List.of(prefix);
     }
 
-    public String getPrefix() {
-      return prefix;
+    StorageType(List<String> prefixes) {
+      this.prefixes = prefixes;
+    }
+
+    public List<String> getPrefixes() {
+      return prefixes;
     }
   }
 
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java
new file mode 100644
index 00000000..51def1fb
--- /dev/null
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageLocation.java
@@ -0,0 +1,90 @@
+/*
+ * 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.storage;
+
+import jakarta.validation.constraints.NotNull;
+import java.net.URI;
+import org.apache.polaris.core.storage.azure.AzureLocation;
+
+/** An abstraction over a storage location */
+public class StorageLocation {
+  private final String location;
+
+  /** Create a StorageLocation from a String path */
+  public static StorageLocation of(String location) {
+    // TODO implement StorageLocation for all supported file systems and add 
isValidLocation
+    if (AzureLocation.isAzureLocation(location)) {
+      return new AzureLocation(location);
+    } else {
+      return new StorageLocation(location);
+    }
+  }
+
+  protected StorageLocation(@NotNull String location) {
+    if (location == null) {
+      this.location = null;
+    } else if (location.startsWith("file:/") && 
!location.startsWith("file:///")) {
+      this.location = URI.create(location.replaceFirst("file:/+", 
"file:///")).toString();
+    } else {
+      this.location = URI.create(location).toString();
+    }
+  }
+
+  /** If a path doesn't end in `/`, this will add one */
+  protected final String ensureTrailingSlash(String location) {
+    if (location == null || location.endsWith("/")) {
+      return location;
+    } else {
+      return location + "/";
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return location.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof StorageLocation) {
+      return location.equals(((StorageLocation) obj).location);
+    } else {
+      return false;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return location;
+  }
+
+  /**
+   * Returns true if this StorageLocation's location string starts with the 
other StorageLocation's
+   * location string
+   */
+  public boolean isChildOf(StorageLocation potentialParent) {
+    if (this.location == null || potentialParent.location == null) {
+      return false;
+    } else {
+      String slashTerminatedLocation = ensureTrailingSlash(this.location);
+      String slashTerminatedParentLocation = 
ensureTrailingSlash(potentialParent.location);
+      return slashTerminatedLocation.startsWith(slashTerminatedParentLocation);
+    }
+  }
+}
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java
index 80b86f53..e7211593 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureLocation.java
@@ -20,17 +20,18 @@ package org.apache.polaris.core.storage.azure;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.apache.polaris.core.storage.StorageLocation;
 import org.jetbrains.annotations.NotNull;
 
 /** This class represents all information for a azure location */
-public class AzureLocation {
-  /** The pattern only allows abfs[s] now because the ResovlingFileIO only 
accept ADLSFileIO */
-  private static final Pattern URI_PATTERN = 
Pattern.compile("^abfss?://([^/?#]+)(.*)?$");
+public class AzureLocation extends StorageLocation {
+  private static final Pattern URI_PATTERN = 
Pattern.compile("^(abfss?|wasbs?)://([^/?#]+)(.*)?$");
 
   public static final String ADLS_ENDPOINT = "dfs.core.windows.net";
 
   public static final String BLOB_ENDPOINT = "blob.core.windows.net";
 
+  private final String scheme;
   private final String storageAccount;
   private final String container;
 
@@ -40,16 +41,19 @@ public class AzureLocation {
   /**
    * Construct an Azure location object from a location uri, it should follow 
this pattern:
    *
-   * <p>{@code abfs[s]://[<container>@]<storage account host>/<file path>}
+   * <p>{@code (abfs|wasb)[s]://[<container>@]<storage account host>/<file 
path>}
    *
    * @param location a uri
    */
   public AzureLocation(@NotNull String location) {
+    super(location);
+
     Matcher matcher = URI_PATTERN.matcher(location);
     if (!matcher.matches()) {
-      throw new IllegalArgumentException("Invalid azure adls location uri " + 
location);
+      throw new IllegalArgumentException("Invalid azure location uri " + 
location);
     }
-    String authority = matcher.group(1);
+    this.scheme = matcher.group(1);
+    String authority = matcher.group(2);
     // look for <container>@<storage account host>
     String[] parts = authority.split("@", -1);
 
@@ -65,7 +69,7 @@ public class AzureLocation {
     }
     this.storageAccount = hostParts[0];
     this.endpoint = hostParts[1];
-    String path = matcher.group(2);
+    String path = matcher.group(3);
     filePath = path == null ? "" : path.startsWith("/") ? path.substring(1) : 
path;
   }
 
@@ -88,4 +92,37 @@ public class AzureLocation {
   public String getFilePath() {
     return filePath;
   }
+
+  /** Get the scheme */
+  public String getScheme() {
+    return scheme;
+  }
+
+  /**
+   * Returns true if the object this StorageLocation refers to is a child of 
the object referred to
+   * by the other StorageLocation.
+   */
+  @Override
+  public boolean isChildOf(@NotNull StorageLocation potentialParent) {
+    if (potentialParent instanceof AzureLocation) {
+      AzureLocation potentialAzureParent = (AzureLocation) potentialParent;
+      if (this.container.equals(potentialAzureParent.container)) {
+        if (this.storageAccount.equals(potentialAzureParent.storageAccount)) {
+          String slashTerminatedFilePath = ensureTrailingSlash(this.filePath);
+          String slashTerminatedParentFilePath = 
ensureTrailingSlash(potentialAzureParent.filePath);
+          return 
slashTerminatedFilePath.startsWith(slashTerminatedParentFilePath);
+        }
+      }
+    }
+    return false;
+  }
+
+  /** Return true if the input location appears to be an Azure path */
+  public static boolean isAzureLocation(String location) {
+    if (location == null) {
+      return false;
+    }
+    Matcher matcher = URI_PATTERN.matcher(location);
+    return matcher.matches();
+  }
 }
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/StorageUtilTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/StorageUtilTest.java
index 8e842d24..99a80d75 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/StorageUtilTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/StorageUtilTest.java
@@ -31,7 +31,7 @@ public class StorageUtilTest {
   }
 
   @ParameterizedTest
-  @ValueSource(strings = {"s3", "gcs", "abfs", "file"})
+  @ValueSource(strings = {"s3", "gcs", "abfs", "wasb", "file"})
   public void testAbsolutePaths(String scheme) {
     Assertions.assertThat(StorageUtil.getBucket(scheme + 
"://bucket/path/file.txt"))
         .isEqualTo("bucket");
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
index 6f0b564b..f152bd88 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
@@ -496,6 +496,6 @@ class AwsCredentialsStorageIntegrationTest {
 
   private static @NotNull String s3Path(
       String bucket, String keyPrefix, 
PolarisStorageConfigurationInfo.StorageType storageType) {
-    return storageType.getPrefix() + bucket + "/" + keyPrefix;
+    return "s3://" + bucket + "/" + keyPrefix;
   }
 }
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java
index 7ec4a910..fd7ac9a8 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java
@@ -65,10 +65,13 @@ public class AzureCredentialStorageIntegrationTest {
 
   private final String clientId = System.getenv("AZURE_CLIENT_ID");
   private final String clientSecret = System.getenv("AZURE_CLIENT_SECRET");
-  private final String tenantId = "d479c7c9-2632-445a-b22d-7c19e68774f6";
+  private final String tenantId = System.getenv("AZURE_TENANT_ID");
 
   private void assumeEnvVariablesNotNull() {
-    Assumptions.assumeThat(Strings.isNullOrEmpty(clientId) || 
Strings.isNullOrEmpty(clientSecret))
+    Assumptions.assumeThat(
+            Strings.isNullOrEmpty(clientId)
+                || Strings.isNullOrEmpty(clientSecret)
+                || Strings.isNullOrEmpty(tenantId))
         .describedAs("Null Azure testing environment variables!")
         .isFalse();
   }
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureLocationTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureLocationTest.java
index f08ae800..8040d289 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureLocationTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureLocationTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.polaris.service.storage.azure;
 
+import org.apache.polaris.core.storage.StorageLocation;
 import org.apache.polaris.core.storage.azure.AzureLocation;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -34,12 +35,28 @@ public class AzureLocationTest {
     Assertions.assertThat(azureLocation.getFilePath()).isEqualTo("myfile");
   }
 
+  @Test
+  public void testWasbLocation() {
+    String uri = 
"wasb://[email protected]/myfile";
+    AzureLocation azureLocation = new AzureLocation(uri);
+    Assertions.assertThat(azureLocation.getContainer()).isEqualTo("container");
+    
Assertions.assertThat(azureLocation.getStorageAccount()).isEqualTo("storageaccount");
+    
Assertions.assertThat(azureLocation.getEndpoint()).isEqualTo("blob.core.windows.net");
+    Assertions.assertThat(azureLocation.getFilePath()).isEqualTo("myfile");
+  }
+
+  @Test
+  public void testCrossSchemeComparisons() {
+    StorageLocation abfsLocation =
+        
AzureLocation.of("abfss://[email protected]/some/file/x");
+    StorageLocation wasbLocation =
+        
AzureLocation.of("wasb://[email protected]/some/file");
+    Assertions.assertThat(abfsLocation).isNotEqualTo(wasbLocation);
+    Assertions.assertThat(abfsLocation.isChildOf(wasbLocation)).isTrue();
+  }
+
   @Test
   public void testLocation_negative_cases() {
-    Assertions.assertThatThrownBy(
-            () ->
-                new 
AzureLocation("wasbs://[email protected]/myfile"))
-        .isInstanceOf(IllegalArgumentException.class);
     Assertions.assertThatThrownBy(
             () -> new 
AzureLocation("abfss://storageaccount.blob.core.windows.net/myfile"))
         .isInstanceOf(IllegalArgumentException.class);
diff --git 
a/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
 
b/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
index c0dfcd81..9c93f3e5 100644
--- 
a/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
+++ 
b/polaris-service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
@@ -78,6 +78,7 @@ import 
org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.core.storage.StorageLocation;
 import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
 import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
 import org.jetbrains.annotations.NotNull;
@@ -517,17 +518,18 @@ public class PolarisAdminService {
                 return false;
               }
               return getCatalogLocations(existingCatalog).stream()
+                  .map(StorageLocation::of)
                   .anyMatch(
-                      existingLocation ->
-                          newCatalogLocations.stream()
-                              .anyMatch(
-                                  newLocation -> {
-                                    if (newLocation == null || 
existingLocation == null) {
-                                      return false;
-                                    }
-                                    return 
newLocation.startsWith(existingLocation)
-                                        || 
existingLocation.startsWith(newLocation);
-                                  }));
+                      existingLocation -> {
+                        return newCatalogLocations.stream()
+                            .anyMatch(
+                                newLocationString -> {
+                                  StorageLocation newLocation =
+                                      StorageLocation.of(newLocationString);
+                                  return 
newLocation.isChildOf(existingLocation)
+                                      || 
existingLocation.isChildOf(newLocation);
+                                });
+                      });
             });
   }
 
diff --git 
a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
 
b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
index 7b687e9a..fc552a5b 100644
--- 
a/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
+++ 
b/polaris-service/src/main/java/org/apache/polaris/service/catalog/BasePolarisCatalog.java
@@ -25,7 +25,6 @@ import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import java.io.Closeable;
 import java.io.IOException;
-import java.net.URI;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -98,6 +97,7 @@ import 
org.apache.polaris.core.storage.InMemoryStorageIntegration;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.PolarisStorageIntegration;
+import org.apache.polaris.core.storage.StorageLocation;
 import org.apache.polaris.core.storage.aws.PolarisS3FileIOClientFactory;
 import org.apache.polaris.service.task.TaskExecutor;
 import org.apache.polaris.service.types.NotificationRequest;
@@ -1125,6 +1125,8 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
           "Unable to resolve sibling entities to validate location - could not 
resolve"
               + status.getFailedToResolvedEntityName());
     }
+
+    StorageLocation targetLocation = StorageLocation.of(location);
     Stream.concat(
             siblingTables.stream()
                 .filter(tbl -> !tbl.name().equals(name))
@@ -1145,15 +1147,14 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
                           .getBaseLocation();
                     }))
         .filter(java.util.Objects::nonNull)
+        .map(StorageLocation::of)
         .forEach(
             siblingLocation -> {
-              URI target = URI.create(location);
-              URI existing = URI.create(siblingLocation);
-              if (isUnderParentLocation(target, existing)
-                  || isUnderParentLocation(existing, target)) {
+              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'",
-                    target, existing);
+                    targetLocation, siblingLocation);
               }
             });
   }
@@ -1406,9 +1407,9 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
           metadata.location(),
           identifier,
           metadata.metadataFileLocation());
-      if (!isUnderParentLocation(
-          URI.create(metadata.metadataFileLocation()),
-          URI.create(metadata.location() + "/metadata").normalize())) {
+      StorageLocation metadataFileLocation = 
StorageLocation.of(metadata.metadataFileLocation());
+      StorageLocation baseLocation = StorageLocation.of(metadata.location());
+      if (!metadataFileLocation.isChildOf(baseLocation)) {
         throw new BadRequestException(
             "Metadata location %s is not allowed outside of table location %s",
             metadata.metadataFileLocation(), metadata.location());
@@ -1792,10 +1793,6 @@ public class BasePolarisCatalog extends 
BaseMetastoreViewCatalog
     }
   }
 
-  private static boolean isUnderParentLocation(URI childLocation, URI 
expectedParentLocation) {
-    return 
!expectedParentLocation.relativize(childLocation).equals(childLocation);
-  }
-
   private void updateTableLike(TableIdentifier identifier, PolarisEntity 
entity) {
     PolarisResolvedPathWrapper resolvedEntities =
         resolvedEntityView.getResolvedPath(identifier, entity.getSubType());
diff --git 
a/polaris-service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
 
b/polaris-service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
index 1381ea0e..167a8768 100644
--- 
a/polaris-service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
+++ 
b/polaris-service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java
@@ -56,7 +56,7 @@ public class CatalogEntityTest {
     Assertions.assertThatThrownBy(() -> CatalogEntity.fromCatalog(awsCatalog))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessageContaining(
-            "Location prefix not allowed: 'unsupportPrefix://mybucket/path', 
expected prefix: 's3://'");
+            "Location prefix not allowed: 'unsupportPrefix://mybucket/path', 
expected prefixes");
 
     // Invalid azure prefix
     AzureStorageConfigInfo azureStorageConfigModel =
@@ -76,7 +76,7 @@ public class CatalogEntityTest {
             .build();
     Assertions.assertThatThrownBy(() -> 
CatalogEntity.fromCatalog(azureCatalog))
         .isInstanceOf(IllegalArgumentException.class)
-        .hasMessageContaining("Invalid azure adls location uri 
unsupportPrefix://mybucket/path");
+        .hasMessageContaining("Invalid azure location uri 
unsupportPrefix://mybucket/path");
 
     // invalid gcp prefix
     GcpStorageConfigInfo gcpStorageConfigModel =
@@ -94,7 +94,7 @@ public class CatalogEntityTest {
     Assertions.assertThatThrownBy(() -> CatalogEntity.fromCatalog(gcpCatalog))
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessageContaining(
-            "Location prefix not allowed: 'unsupportPrefix://mybucket/path', 
expected prefix: 'gs://'");
+            "Location prefix not allowed: 'unsupportPrefix://mybucket/path', 
expected prefixes");
   }
 
   @Test

Reply via email to