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

dweeks pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new 8bb66f7071 Core: Deprecate Namespace Joiner/Splitter and use separate 
methods (#14274)
8bb66f7071 is described below

commit 8bb66f7071cacfb69f37d72807836b5a841df0ea
Author: Eduard Tudenhoefner <[email protected]>
AuthorDate: Wed Oct 8 17:20:35 2025 +0200

    Core: Deprecate Namespace Joiner/Splitter and use separate methods (#14274)
    
    This revisits https://github.com/apache/iceberg/pull/10858 (which was 
reverted in https://github.com/apache/iceberg/pull/11574) and uses separate 
method to join/split a namespace using the unicode character \001f.
    We eventually want to make the namespace separator configurable and in 
order to do that, all namespace joining/splitting needs to go through 
respective methods instead of using the Joiner/Splitter directly
---
 .../apache/iceberg/rest/RESTSessionCatalog.java    |  2 +-
 .../java/org/apache/iceberg/rest/RESTUtil.java     | 49 +++++++++++++++++++++-
 .../apache/iceberg/rest/RESTCatalogAdapter.java    |  6 +--
 .../java/org/apache/iceberg/rest/TestRESTUtil.java | 46 ++++++++++++++++++++
 4 files changed, 95 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
index 4692b92b51..b903f13adc 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java
@@ -570,7 +570,7 @@ public class RESTSessionCatalog extends 
BaseViewSessionCatalog
 
     Map<String, String> queryParams = Maps.newHashMap();
     if (!namespace.isEmpty()) {
-      queryParams.put("parent", 
RESTUtil.NAMESPACE_JOINER.join(namespace.levels()));
+      queryParams.put("parent", RESTUtil.namespaceToQueryParam(namespace));
     }
 
     ImmutableList.Builder<Namespace> namespaces = ImmutableList.builder();
diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java 
b/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java
index 01b57a4151..d31260a918 100644
--- a/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java
+++ b/core/src/main/java/org/apache/iceberg/rest/RESTUtil.java
@@ -32,13 +32,23 @@ import 
org.apache.iceberg.relocated.com.google.common.collect.Maps;
 
 public class RESTUtil {
   private static final char NAMESPACE_SEPARATOR = '\u001f';
-  public static final Joiner NAMESPACE_JOINER = Joiner.on(NAMESPACE_SEPARATOR);
-  public static final Splitter NAMESPACE_SPLITTER = 
Splitter.on(NAMESPACE_SEPARATOR);
   private static final String NAMESPACE_ESCAPED_SEPARATOR = "%1F";
   private static final Joiner NAMESPACE_ESCAPED_JOINER = 
Joiner.on(NAMESPACE_ESCAPED_SEPARATOR);
   private static final Splitter NAMESPACE_ESCAPED_SPLITTER =
       Splitter.on(NAMESPACE_ESCAPED_SEPARATOR);
 
+  /**
+   * @deprecated since 1.11.0, will be removed in 1.12.0; use {@link
+   *     RESTUtil#namespaceToQueryParam(Namespace)}} instead.
+   */
+  @Deprecated public static final Joiner NAMESPACE_JOINER = 
Joiner.on(NAMESPACE_SEPARATOR);
+
+  /**
+   * @deprecated since 1.11.0, will be removed in 1.12.0; use {@link
+   *     RESTUtil#namespaceFromQueryParam(String)} instead.
+   */
+  @Deprecated public static final Splitter NAMESPACE_SPLITTER = 
Splitter.on(NAMESPACE_SEPARATOR);
+
   private RESTUtil() {}
 
   public static String stripTrailingSlash(String path) {
@@ -160,6 +170,41 @@ public class RESTUtil {
     return URLDecoder.decode(encoded, StandardCharsets.UTF_8);
   }
 
+  /**
+   * This converts the given namespace to a string and separates each part in 
a multipart namespace
+   * using the unicode character '\u001f'. Note that this method is different 
from {@link
+   * RESTUtil#encodeNamespace(Namespace)}, which uses the UTF-8 escaped 
version of '\u001f', which
+   * is '0x1F'.
+   *
+   * <p>{@link #namespaceFromQueryParam(String)} should be used to convert the 
namespace string back
+   * to a {@link Namespace} instance.
+   *
+   * @param namespace The namespace to convert
+   * @return The namespace converted to a string where each part in a 
multipart namespace is
+   *     separated using the unicode character '\u001f'
+   */
+  public static String namespaceToQueryParam(Namespace namespace) {
+    Preconditions.checkArgument(null != namespace, "Invalid namespace: null");
+    return RESTUtil.NAMESPACE_JOINER.join(namespace.levels());
+  }
+
+  /**
+   * This converts a namespace where each part in a multipart namespace has 
been separated using
+   * '\u001f' to its original {@link Namespace} instance.
+   *
+   * <p>{@link #namespaceToQueryParam(Namespace)} should be used to convert 
the {@link Namespace} to
+   * a string.
+   *
+   * @param namespace The namespace to convert
+   * @return The namespace instance from the given namespace string, where 
each multipart separator
+   *     ('\u001f') is converted to a separate namespace level
+   */
+  public static Namespace namespaceFromQueryParam(String namespace) {
+    Preconditions.checkArgument(null != namespace, "Invalid namespace: null");
+    return Namespace.of(
+        
RESTUtil.NAMESPACE_SPLITTER.splitToStream(namespace).toArray(String[]::new));
+  }
+
   /**
    * Returns a String representation of a namespace that is suitable for use 
in a URL / URI.
    *
diff --git a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java 
b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
index 35ef6dac35..675aa8ab6f 100644
--- a/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
+++ b/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java
@@ -314,11 +314,7 @@ public class RESTCatalogAdapter extends BaseHTTPClient {
         if (asNamespaceCatalog != null) {
           Namespace ns;
           if (vars.containsKey("parent")) {
-            ns =
-                Namespace.of(
-                    RESTUtil.NAMESPACE_SPLITTER
-                        .splitToStream(vars.get("parent"))
-                        .toArray(String[]::new));
+            ns = RESTUtil.namespaceFromQueryParam(vars.get("parent"));
           } else {
             ns = Namespace.empty();
           }
diff --git a/core/src/test/java/org/apache/iceberg/rest/TestRESTUtil.java 
b/core/src/test/java/org/apache/iceberg/rest/TestRESTUtil.java
index 6eb8f59bf6..fe022b3518 100644
--- a/core/src/test/java/org/apache/iceberg/rest/TestRESTUtil.java
+++ b/core/src/test/java/org/apache/iceberg/rest/TestRESTUtil.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.rest;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.Map;
 import org.apache.iceberg.catalog.Namespace;
@@ -166,4 +167,49 @@ public class TestRESTUtil {
             RESTUtil.resolveEndpoint("http://catalog-uri/";, 
"relative-endpoint/refresh-endpoint"))
         .isEqualTo("http://catalog-uri/relative-endpoint/refresh-endpoint";);
   }
+
+  @Test
+  public void namespaceToQueryParam() {
+    assertThatThrownBy(() -> RESTUtil.namespaceToQueryParam(null))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Invalid namespace: null");
+
+    
assertThat(RESTUtil.namespaceToQueryParam(Namespace.empty())).isEqualTo("");
+    assertThat(RESTUtil.namespaceToQueryParam(Namespace.of(""))).isEqualTo("");
+    
assertThat(RESTUtil.namespaceToQueryParam(Namespace.of("ns"))).isEqualTo("ns");
+
+    // verify that the unicode character (\001f) and not its UTF-8 escaped 
version (%1F) is used
+    assertThat(RESTUtil.namespaceToQueryParam(Namespace.of("one", "ns")))
+        .isEqualTo("one\u001fns")
+        .isNotEqualTo("one%1Fns");
+    assertThat(RESTUtil.namespaceToQueryParam(Namespace.of("one", "two", 
"ns")))
+        .isEqualTo("one\u001ftwo\u001fns")
+        .isNotEqualTo("one%1Ftwo%1Fns");
+    assertThat(RESTUtil.namespaceToQueryParam(Namespace.of("one.two", 
"three.four", "ns")))
+        .isEqualTo("one.two\u001fthree.four\u001fns")
+        .isNotEqualTo("one.two%1Fthree.four%1Fns");
+  }
+
+  @Test
+  public void namespaceFromQueryParam() {
+    assertThatThrownBy(() -> RESTUtil.namespaceFromQueryParam(null))
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Invalid namespace: null");
+
+    
assertThat(RESTUtil.namespaceFromQueryParam("")).isEqualTo(Namespace.of(""));
+    
assertThat(RESTUtil.namespaceFromQueryParam("ns")).isEqualTo(Namespace.of("ns"));
+    assertThat(RESTUtil.namespaceFromQueryParam("one\u001fns"))
+        .isEqualTo(Namespace.of("one", "ns"));
+    assertThat(RESTUtil.namespaceFromQueryParam("one\u001ftwo\u001fns"))
+        .isEqualTo(Namespace.of("one", "two", "ns"));
+    
assertThat(RESTUtil.namespaceFromQueryParam("one.two\u001fthree.four\u001fns"))
+        .isEqualTo(Namespace.of("one.two", "three.four", "ns"));
+
+    // using the UTF-8 escaped version will produce a wrong namespace instance
+    
assertThat(RESTUtil.namespaceFromQueryParam("one%1Fns")).isEqualTo(Namespace.of("one%1Fns"));
+    assertThat(RESTUtil.namespaceFromQueryParam("one%1Ftwo%1Fns"))
+        .isEqualTo(Namespace.of("one%1Ftwo%1Fns"));
+    assertThat(RESTUtil.namespaceFromQueryParam("one%1Ftwo\u001fns"))
+        .isEqualTo(Namespace.of("one%1Ftwo", "ns"));
+  }
 }

Reply via email to