You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by ad...@apache.org on 2026/05/21 12:08:36 UTC

(polaris) branch main updated: Replace deprecated encodeNamespace and decodeNamespace methods (cherry-pick of #3713) (#4508)

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

adutra 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 f2eb8ad225 Replace deprecated encodeNamespace and decodeNamespace methods (cherry-pick of #3713) (#4508)
f2eb8ad225 is described below

commit f2eb8ad225d7ef8a50cbf53bf582365e75027334
Author: Alexandre Dutra <ad...@apache.org>
AuthorDate: Thu May 21 14:08:30 2026 +0200

    Replace deprecated encodeNamespace and decodeNamespace methods (cherry-pick of #3713) (#4508)
---
 .../test/PolarisPolicyServiceIntegrationTest.java  |  8 +++--
 .../it/test/PolarisRestCatalogIntegrationBase.java | 29 +++++++++++------
 .../spark/v3.5/regtests/suites/spark_sql_delta.ref |  2 +-
 .../spark/v3.5/regtests/suites/spark_sql_hudi.ref  |  2 +-
 .../polaris/core/entity/PolarisEntityUtils.java    |  4 +--
 .../apache/polaris/core/rest/NamespaceUtils.java   | 13 ++++++++
 .../polaris/core/rest/PolarisResourcePaths.java    | 38 +++++++++++++++++++---
 .../core/entity/PolarisEntityUtilsTest.java        |  2 ++
 .../t_spark_sql/ref/spark_sql_azure_blob.sh.ref    |  2 +-
 .../t_spark_sql/ref/spark_sql_azure_dfs.sh.ref     |  2 +-
 regtests/t_spark_sql/ref/spark_sql_basic.sh.ref    |  2 +-
 regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref      |  2 +-
 regtests/t_spark_sql/ref/spark_sql_s3.sh.ref       |  2 +-
 regtests/t_spark_sql/ref/spark_sql_views.sh.ref    |  2 +-
 .../catalog/iceberg/IcebergCatalogAdapter.java     |  8 +++--
 .../catalog/iceberg/IcebergCatalogHandler.java     | 12 +++++--
 16 files changed, 98 insertions(+), 32 deletions(-)

diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
index f8016508a9..1389d019a8 100644
--- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
+++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
@@ -38,7 +38,6 @@ import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.rest.RESTCatalog;
-import org.apache.iceberg.rest.RESTUtil;
 import org.apache.iceberg.types.Types;
 import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
 import org.apache.polaris.core.admin.model.Catalog;
@@ -61,6 +60,7 @@ import org.apache.polaris.core.admin.model.TablePrivilege;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
 import org.apache.polaris.core.policy.PredefinedPolicyTypes;
 import org.apache.polaris.core.policy.exceptions.PolicyInUseException;
+import org.apache.polaris.core.rest.NamespaceUtils;
 import org.apache.polaris.service.it.env.CatalogConfig;
 import org.apache.polaris.service.it.env.ClientCredentials;
 import org.apache.polaris.service.it.env.IcebergHelper;
@@ -109,7 +109,7 @@ public class PolarisPolicyServiceIntegrationTest {
   private static final PolicyIdentifier NS1_P3 = new PolicyIdentifier(NS1, "P3");
   private static final TableIdentifier NS2_T1 = TableIdentifier.of(NS2, "T1");
 
-  private static final String NS1_NAME = RESTUtil.encodeNamespace(NS1);
+  private static final String NS1_NAME = NS1.level(0);
   private static final String INVALID_NAMESPACE = "INVALID_NAMESPACE";
   private static final String INVALID_POLICY = "INVALID_POLICY";
   private static final String INVALID_TABLE = "INVALID_TABLE";
@@ -290,7 +290,9 @@ public class PolarisPolicyServiceIntegrationTest {
     restCatalog.createNamespace(NS1);
     PolicyIdentifier policyIdentifier = new PolicyIdentifier(NS1, policyName);
 
-    String ns = RESTUtil.encodeNamespace(policyIdentifier.namespace());
+    String ns =
+        NamespaceUtils.joinNamespace(
+            policyIdentifier.namespace(), NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
     CreatePolicyRequest request =
         CreatePolicyRequest.builder()
             .setType(PredefinedPolicyTypes.DATA_COMPACTION.getName())
diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
index f879d81d90..0f02e3f8a7 100644
--- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
+++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
@@ -97,6 +97,7 @@ import org.apache.polaris.core.admin.model.ViewGrant;
 import org.apache.polaris.core.admin.model.ViewPrivilege;
 import org.apache.polaris.core.config.FeatureConfiguration;
 import org.apache.polaris.core.entity.PolarisEntityConstants;
+import org.apache.polaris.core.rest.NamespaceUtils;
 import org.apache.polaris.service.it.env.CatalogApi;
 import org.apache.polaris.service.it.env.CatalogConfig;
 import org.apache.polaris.service.it.env.ClientCredentials;
@@ -1669,7 +1670,9 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     TableIdentifier tableIdentifier = TableIdentifier.of(namespace, "tbl1");
 
     try {
-      String ns = RESTUtil.encodeNamespace(tableIdentifier.namespace());
+      String ns =
+          NamespaceUtils.joinNamespace(
+              tableIdentifier.namespace(), NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
       try (Response res =
           genericTableApi
               .request(
@@ -1750,7 +1753,9 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     restCatalog.createNamespace(namespace);
     TableIdentifier tableIdentifier = TableIdentifier.of(namespace, "tbl1");
 
-    String ns = RESTUtil.encodeNamespace(tableIdentifier.namespace());
+    String ns =
+        NamespaceUtils.joinNamespace(
+            tableIdentifier.namespace(), NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
     try (Response res =
         genericTableApi
             .request(
@@ -2454,12 +2459,13 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace ns = Namespace.of("ns_create_table_bad");
     restCatalog.createNamespace(ns);
     try {
-      String nsEncoded = RESTUtil.encodeNamespace(ns);
+      String nsJoined =
+          NamespaceUtils.joinNamespace(ns, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
       try (Response res =
           catalogApi
               .request(
                   "v1/{cat}/namespaces/{ns}/tables",
-                  Map.of("cat", currentCatalogName, "ns", nsEncoded))
+                  Map.of("cat", currentCatalogName, "ns", nsJoined))
               .post(Entity.json(Map.of("name", badName, "schema", SCHEMA)))) {
         assertThat(res.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
         assertThat(res.readEntity(String.class)).contains("Entity name");
@@ -2475,12 +2481,13 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace ns = Namespace.of("ns_register_bad");
     restCatalog.createNamespace(ns);
     try {
-      String nsEncoded = RESTUtil.encodeNamespace(ns);
+      String nsJoined =
+          NamespaceUtils.joinNamespace(ns, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
       try (Response res =
           catalogApi
               .request(
                   "v1/{cat}/namespaces/{ns}/register",
-                  Map.of("cat", currentCatalogName, "ns", nsEncoded))
+                  Map.of("cat", currentCatalogName, "ns", nsJoined))
               .post(
                   Entity.json(
                       Map.of("name", badName, "metadata-location", "file:/tmp/nowhere.json")))) {
@@ -2498,7 +2505,8 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace ns = Namespace.of("ns_create_view_bad");
     restCatalog.createNamespace(ns);
     try {
-      String nsEncoded = RESTUtil.encodeNamespace(ns);
+      String nsJoined =
+          NamespaceUtils.joinNamespace(ns, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
       Map<String, Object> viewVersion =
           Map.of(
               "version-id",
@@ -2517,7 +2525,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
           catalogApi
               .request(
                   "v1/{cat}/namespaces/{ns}/views",
-                  Map.of("cat", currentCatalogName, "ns", nsEncoded))
+                  Map.of("cat", currentCatalogName, "ns", nsJoined))
               .post(
                   Entity.json(
                       Map.of("name", badName, "schema", SCHEMA, "view-version", viewVersion)))) {
@@ -2588,12 +2596,13 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace ns = Namespace.of("ns_generic_bad");
     restCatalog.createNamespace(ns);
     try {
-      String nsEncoded = RESTUtil.encodeNamespace(ns);
+      String nsJoined =
+          NamespaceUtils.joinNamespace(ns, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
       try (Response res =
           genericTableApi
               .request(
                   "polaris/v1/{cat}/namespaces/{ns}/generic-tables",
-                  Map.of("cat", currentCatalogName, "ns", nsEncoded))
+                  Map.of("cat", currentCatalogName, "ns", nsJoined))
               .post(
                   Entity.json(
                       CreateGenericTableRequest.builder()
diff --git a/plugins/spark/v3.5/regtests/suites/spark_sql_delta.ref b/plugins/spark/v3.5/regtests/suites/spark_sql_delta.ref
index 01aa77e6d1..5daf5d344e 100755
--- a/plugins/spark/v3.5/regtests/suites/spark_sql_delta.ref
+++ b/plugins/spark/v3.5/regtests/suites/spark_sql_delta.ref
@@ -1,4 +1,4 @@
-{"defaults":{"default-base-location":"file:///tmp/spark_catalog"},"overrides":{"prefix":"spark_sql_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD /v1/{prefix}/ [...]
+{"defaults":{"default-base-location":"file:///tmp/spark_catalog"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/ [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> create namespace db1;
diff --git a/plugins/spark/v3.5/regtests/suites/spark_sql_hudi.ref b/plugins/spark/v3.5/regtests/suites/spark_sql_hudi.ref
index a40b4ddac9..df9179e645 100644
--- a/plugins/spark/v3.5/regtests/suites/spark_sql_hudi.ref
+++ b/plugins/spark/v3.5/regtests/suites/spark_sql_hudi.ref
@@ -1,4 +1,4 @@
-{"defaults":{"default-base-location":"file:///tmp/spark_hudi_catalog"},"overrides":{"prefix":"spark_hudi_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD /v1/{pr [...]
+{"defaults":{"default-base-location":"file:///tmp/spark_hudi_catalog"},"overrides":{"namespace-separator":"%1F","prefix":"spark_hudi_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/t [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> create namespace hudi_db1;
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 e9a2132c5d..2dd360172b 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
@@ -52,7 +52,7 @@ public final class PolarisEntityUtils {
    * Returns a String representation of a namespace that is suitable for storage in entity internal
    * properties.
    *
-   * <p>This method is similar to {@link RESTUtil#encodeNamespace(Namespace)}.
+   * <p>This method is similar to {@link RESTUtil#encodeNamespace(Namespace, String)}.
    */
   public static String encodeNamespace(Namespace ns) {
     Preconditions.checkArgument(ns != null, "Invalid namespace: null");
@@ -68,7 +68,7 @@ public final class PolarisEntityUtils {
    * Returns a Namespace object from a String representation that was encoded using {@link
    * #encodeNamespace(Namespace)}.
    *
-   * <p>This method is similar to {@link RESTUtil#decodeNamespace(String)}.
+   * <p>This method is similar to {@link RESTUtil#decodeNamespace(String, String)}.
    */
   public static Namespace decodeNamespace(String encodedNs) {
     Preconditions.checkArgument(encodedNs != null, "Invalid namespace: null");
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/NamespaceUtils.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/NamespaceUtils.java
index 3de87edf06..64c06b5b72 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/rest/NamespaceUtils.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/NamespaceUtils.java
@@ -35,6 +35,19 @@ public final class NamespaceUtils {
    */
   public static final String DEFAULT_NAMESPACE_SEPARATOR = "\u001f";
 
+  /**
+   * The default namespace separator, percent-encoded to "%1F".
+   *
+   * <p>This is the same encoded separator declared in Iceberg's {@link
+   * org.apache.iceberg.rest.RESTUtil} class, but it's package-private there and cannot be
+   * referenced.
+   *
+   * <p>This value is also exposed in {@link
+   * org.apache.iceberg.rest.RESTCatalogProperties#NAMESPACE_SEPARATOR_DEFAULT} – but here we avoid
+   * referencing that field directly since we don't own it.
+   */
+  public static final String DEFAULT_NAMESPACE_SEPARATOR_ENCODED = "%1F";
+
   private NamespaceUtils() {}
 
   /** Joins the levels of a namespace into a single string using the given {@code separator}. */
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java
index 16eea08da2..87e29bbd99 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java
@@ -22,9 +22,15 @@ import com.google.common.base.Joiner;
 import java.util.Map;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTCatalogProperties;
 import org.apache.iceberg.rest.RESTUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PolarisResourcePaths {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(PolarisResourcePaths.class);
+
   private static final Joiner SLASH = Joiner.on("/").skipNulls();
   public static final String PREFIX = "prefix";
 
@@ -43,18 +49,34 @@ public class PolarisResourcePaths {
   public static final String V1_APPLICABLE_POLICIES = "/polaris/v1/{prefix}/applicable-policies";
 
   private final String prefix;
+  private final String namespaceSeparatorEncoded;
 
-  public PolarisResourcePaths(String prefix) {
+  public PolarisResourcePaths(String prefix, String namespaceSeparatorEncoded) {
     this.prefix = prefix;
+    this.namespaceSeparatorEncoded = namespaceSeparatorEncoded;
   }
 
   public static PolarisResourcePaths forCatalogProperties(Map<String, String> properties) {
-    return new PolarisResourcePaths(properties.get(PREFIX));
+    String namespaceSeparatorEncoded =
+        properties.getOrDefault(
+            RESTCatalogProperties.NAMESPACE_SEPARATOR,
+            RESTCatalogProperties.NAMESPACE_SEPARATOR_DEFAULT);
+    if (!namespaceSeparatorEncoded.equals(RESTCatalogProperties.NAMESPACE_SEPARATOR_DEFAULT)) {
+      LOGGER.warn("Using non-default namespace separator '{}'", namespaceSeparatorEncoded);
+    }
+    return new PolarisResourcePaths(properties.get(PREFIX), namespaceSeparatorEncoded);
   }
 
   public String genericTables(Namespace ns) {
     return SLASH.join(
-        "polaris", "v1", prefix, "namespaces", RESTUtil.encodeNamespace(ns), "generic-tables");
+        "polaris",
+        "v1",
+        prefix,
+        "namespaces",
+        // FIXME use RESTUtil.encodeNamespaceAsPathSegment(), see
+        // https://github.com/apache/iceberg/pull/15989
+        RESTUtil.encodeNamespace(ns, namespaceSeparatorEncoded),
+        "generic-tables");
   }
 
   public String credentialsPath(TableIdentifier ident) {
@@ -62,8 +84,11 @@ public class PolarisResourcePaths {
         "v1",
         prefix,
         "namespaces",
-        RESTUtil.encodeNamespace(ident.namespace()),
+        // FIXME use RESTUtil.encodeNamespaceAsPathSegment(), see
+        // https://github.com/apache/iceberg/pull/15989
+        RESTUtil.encodeNamespace(ident.namespace(), namespaceSeparatorEncoded),
         "tables",
+        // FIXME use RESTUtil.encodePathSegment(), see https://github.com/apache/iceberg/pull/15989
         RESTUtil.encodeString(ident.name()),
         "credentials");
   }
@@ -74,8 +99,11 @@ public class PolarisResourcePaths {
         "v1",
         prefix,
         "namespaces",
-        RESTUtil.encodeNamespace(ident.namespace()),
+        // FIXME use RESTUtil.encodeNamespaceAsPathSegment(), see
+        // https://github.com/apache/iceberg/pull/15989
+        RESTUtil.encodeNamespace(ident.namespace(), namespaceSeparatorEncoded),
         "generic-tables",
+        // FIXME use RESTUtil.encodePathSegment(), see https://github.com/apache/iceberg/pull/15989
         RESTUtil.encodeString(ident.name()));
   }
 }
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 21400389f2..713993ee47 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
@@ -52,6 +52,7 @@ public class PolarisEntityUtilsTest {
 
   @ParameterizedTest
   @MethodSource("encodeNamespaceCases")
+  @SuppressWarnings("deprecation")
   public void testEncodeNamespace(Namespace ns, String expected) {
     assertThat(PolarisEntityUtils.encodeNamespace(ns))
         .isEqualTo(RESTUtil.encodeNamespace(ns))
@@ -86,6 +87,7 @@ public class PolarisEntityUtilsTest {
 
   @ParameterizedTest
   @MethodSource("decodeNamespaceCases")
+  @SuppressWarnings("deprecation")
   public void testDecodeNamespace(String encoded, Namespace expected) {
     assertThat(PolarisEntityUtils.decodeNamespace(encoded))
         .isEqualTo(RESTUtil.decodeNamespace(encoded))
diff --git a/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref b/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref
index a6d831f814..0b95ef5807 100755
--- a/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_azure_blob.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"abfss://polaris-container@polarisadls.blob.core.windows.net/polaris-test/spark_sql_blob_catalog/"},"overrides":{"prefix":"spark_sql_azure_blob_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{ [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"abfss://polaris-container@polarisadls.blob.core.windows.net/polaris-test/spark_sql_blob_catalog/"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_azure_blob_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namesp [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref b/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref
index b37ef2603f..d0d7e51ae9 100755
--- a/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_azure_dfs.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"abfss://polaris-container@polarisadls.dfs.core.windows.net/polaris-test/spark_sql_dfs_catalog/"},"overrides":{"prefix":"spark_sql_azure_dfs_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{pre [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"abfss://polaris-container@polarisadls.dfs.core.windows.net/polaris-test/spark_sql_dfs_catalog/"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_azure_dfs_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespace [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref
index d9657c7482..f129c395fe 100755
--- a/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_basic_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{ [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_basic_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","G [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref b/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref
index 114169d38d..207ea61c8f 100755
--- a/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_gcp.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"gs://polaris-test1/polaris_test/spark_sql_gcp_catalog/"},"overrides":{"prefix":"spark_sql_gcp_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{ [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"gs://polaris-test1/polaris_test/spark_sql_gcp_catalog/"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_gcp_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{n [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref b/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref
index b55f1648c6..bee0562503 100755
--- a/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_s3.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"s3://test_bucket/polaris_test/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_s3_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefi [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"s3://test_bucket/polaris_test/spark_sql_s3_catalog"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_s3_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namesp [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/regtests/t_spark_sql/ref/spark_sql_views.sh.ref b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref
index cf2b22d196..3ecfb05995 100755
--- a/regtests/t_spark_sql/ref/spark_sql_views.sh.ref
+++ b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref
@@ -1,4 +1,4 @@
-{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_views_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{ [...]
+{"defaults":{"drop-with-purge.enabled":"true","default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"namespace-separator":"%1F","prefix":"spark_sql_views_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","G [...]
 Catalog created
 spark-sql (default)> use polaris;
 spark-sql ()> show namespaces;
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index f48de53c67..f38efeb448 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -370,7 +370,9 @@ public class IcebergCatalogAdapter
       String prefix,
       TableIdentifier tableIdentifier) {
     return delegationModes.contains(AccessDelegationMode.VENDED_CREDENTIALS)
-        ? Optional.of(new PolarisResourcePaths(prefix).credentialsPath(tableIdentifier))
+        ? Optional.of(
+            new PolarisResourcePaths(prefix, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR_ENCODED)
+                .credentialsPath(tableIdentifier))
         : Optional.empty();
   }
 
@@ -548,7 +550,9 @@ public class IcebergCatalogAdapter
         NamespaceUtils.splitNamespace(namespace, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
     TableIdentifier tableIdentifier = TableIdentifier.of(ns, table);
     Optional<String> refreshEndpoint =
-        Optional.of(new PolarisResourcePaths(prefix).credentialsPath(tableIdentifier));
+        Optional.of(
+            new PolarisResourcePaths(prefix, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR_ENCODED)
+                .credentialsPath(tableIdentifier));
     return withCatalog(
         securityContext,
         prefix,
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 575a0010ad..46c0af5915 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -70,6 +70,7 @@ import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.NotFoundException;
 import org.apache.iceberg.metrics.ScanReport;
 import org.apache.iceberg.rest.Endpoint;
+import org.apache.iceberg.rest.RESTCatalogProperties;
 import org.apache.iceberg.rest.credentials.ImmutableCredential;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
@@ -112,6 +113,7 @@ 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.ResolverStatus;
+import org.apache.polaris.core.rest.NamespaceUtils;
 import org.apache.polaris.core.rest.PolarisEndpoints;
 import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.core.storage.StorageAccessConfig;
@@ -1436,10 +1438,16 @@ public abstract class IcebergCatalogHandler extends CatalogHandler implements Au
     Map<String, String> properties =
         PolarisEntity.of(resolvedReferenceCatalog.getEntity()).getPropertiesAsMap();
 
-    String prefix = prefixParser().catalogNameToPrefix(catalogName());
     return ConfigResponse.builder()
         .withDefaults(properties) // catalog properties are defaults
-        .withOverrides(ImmutableMap.of("prefix", prefix))
+        .withOverrides(
+            ImmutableMap.of(
+                "prefix",
+                prefixParser().catalogNameToPrefix(catalogName()),
+                // Polaris does not handle custom namespace separators;
+                // always communicate the default namespace separator to clients.
+                RESTCatalogProperties.NAMESPACE_SEPARATOR,
+                NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR_ENCODED))
         .withEndpoints(
             ImmutableList.<Endpoint>builder()
                 .addAll(DEFAULT_ENDPOINTS)