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/07/16 23:05:32 UTC
(polaris) branch main updated: fix(catalog): encode default table locations with UTF-8 (#4975)
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 e9039e1200 fix(catalog): encode default table locations with UTF-8 (#4975)
e9039e1200 is described below
commit e9039e12003a13e783b5130a3d30d30cfe78d93c
Author: Anas Khan <an...@gmail.com>
AuthorDate: Fri Jul 17 04:35:28 2026 +0530
fix(catalog): encode default table locations with UTF-8 (#4975)
buildPrefixedLocation derived a table's default storage location by
percent-encoding the namespace levels and table name with
URLEncoder.encode(..., Charset.defaultCharset()). For non-ASCII
identifiers the persisted location then depended on the JVM's default
charset, so the same table could resolve to a different (or lossy)
location on a non-UTF-8 JVM, breaking portability and round-trip
expectations.
Encode both segments with StandardCharsets.UTF_8, matching
PolarisEntityUtils.encodeNamespace and every other URLEncoder call site
in the service. Add a regression test that creates a table under
non-ASCII namespace and table identifiers with object-storage prefixing
enabled and asserts the derived location uses the UTF-8 percent-encoded
form regardless of the platform default charset.
Signed-off-by: Anas Khan <83...@users.noreply.github.com>
---
CHANGELOG.md | 1 +
.../catalog/iceberg/LocalIcebergCatalog.java | 6 ++--
.../catalog/iceberg/LocalIcebergCatalogTest.java | 41 ++++++++++++++++++++--
3 files changed, 43 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 844576b217..bbb33f150e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
- Deprecated `ALLOW_EXTERNAL_TABLE_LOCATION`. Use `ALLOW_EXTERNAL_METADATA_FILE_LOCATION` for external metadata file locations, including catalog config `polaris.config.allow.external.metadata.file.location`.
### Fixes
+- Default table storage locations with object-storage prefixing enabled are now percent-encoded with UTF-8 instead of the JVM default charset. Previously the persisted location of a table under a non-ASCII namespace or table name depended on the platform default charset, so the same table could resolve to a different (or lossy) location on a non-UTF-8 JVM.
- Async task execution (table cleanup, manifest and batch file cleanup) now retries when a handler returns false on transient errors (e.g. IO or delete failures). Previously `false` was swallowed with only a warning log and the task was never retried via the existing retry mechanism.
- `TableCleanupTaskHandler` now clamps `TABLE_METADATA_CLEANUP_BATCH_SIZE` to at least 1. Previously a non-positive realm override caused an infinite loop (0) or `IllegalArgumentException` (<0) when splitting metadata files for cleanup.
- `ManifestFileCleanupTaskHandler` now handles Iceberg v2 delete manifests in addition to data manifests. Previously, `DROP TABLE PURGE` on a v2 table that had been updated via merge-on-read DML left position-delete files and their manifests as orphans in object storage; the cleanup task failed silently because `ManifestFiles.read()` rejects delete manifests.
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalog.java
index c4db7a5a7d..5b77cdeb19 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalog.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalog.java
@@ -34,7 +34,7 @@ import com.google.common.collect.ImmutableMap;
import java.io.Closeable;
import java.io.IOException;
import java.net.URLEncoder;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
@@ -1194,11 +1194,11 @@ public class LocalIcebergCatalog extends BaseMetastoreViewCatalog
locationBuilder.append(LocationUtils.computeHash(tableIdentifier.toString()));
for (String ns : tableIdentifier.namespace().levels()) {
- locationBuilder.append("/").append(URLEncoder.encode(ns, Charset.defaultCharset()));
+ locationBuilder.append("/").append(URLEncoder.encode(ns, StandardCharsets.UTF_8));
}
locationBuilder
.append("/")
- .append(URLEncoder.encode(defaultTableLikeName(tableIdentifier), Charset.defaultCharset()))
+ .append(URLEncoder.encode(defaultTableLikeName(tableIdentifier), StandardCharsets.UTF_8))
.append("/");
return locationBuilder.toString();
}
diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogTest.java
index a1890e14d1..53fa29b63b 100644
--- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogTest.java
+++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/LocalIcebergCatalogTest.java
@@ -19,10 +19,18 @@
package org.apache.polaris.service.catalog.iceberg;
+import static org.apache.polaris.core.config.FeatureConfiguration.ALLOW_TABLE_LOCATION_OVERLAP;
+import static org.apache.polaris.core.config.FeatureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION;
+import static org.apache.polaris.core.config.FeatureConfiguration.DEFAULT_LOCATION_OBJECT_STORAGE_PREFIX_ENABLED;
+import static org.apache.polaris.core.config.FeatureConfiguration.DEFAULT_UNIQUE_TABLE_LOCATION_ENABLED;
+import static org.apache.polaris.core.config.FeatureConfiguration.OPTIMIZED_SIBLING_CHECK;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -31,6 +39,7 @@ 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.config.RealmConfig;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.entity.CatalogEntity;
import org.apache.polaris.core.entity.PolarisEntity;
@@ -55,6 +64,7 @@ class LocalIcebergCatalogTest {
@Mock ResolverFactory resolverFactory;
@Mock CallContext callContext;
+ @Mock RealmConfig realmConfig;
@Mock PolarisResolutionManifestCatalogView resolvedEntityView;
@Mock CatalogEntity catalogEntity;
@Mock PolarisDiagnostics diagnostics;
@@ -73,9 +83,13 @@ class LocalIcebergCatalogTest {
@BeforeEach
void initMocks() {
+ when(callContext.getRealmConfig()).thenReturn(realmConfig);
+ when(catalogEntity.getName()).thenReturn("catalog");
when(resolvedEntityView.getResolvedCatalogEntity()).thenReturn(catalogEntity);
- when(resolverFactory.createResolver(any(), any())).thenReturn(resolver);
- when(resolver.resolveAll()).thenReturn(new ResolverStatus(ResolverStatus.StatusEnum.SUCCESS));
+ lenient().when(resolverFactory.createResolver(any(), any())).thenReturn(resolver);
+ lenient()
+ .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());
@@ -96,6 +110,29 @@ class LocalIcebergCatalogTest {
null);
}
+ @Test
+ void testHashedTableLocationsEncodeNonAsciiIdentifiersAsUtf8() {
+ when(realmConfig.getConfig(DEFAULT_LOCATION_OBJECT_STORAGE_PREFIX_ENABLED, catalogEntity))
+ .thenReturn(true);
+ when(realmConfig.getConfig(ALLOW_UNSTRUCTURED_TABLE_LOCATION, catalogEntity)).thenReturn(true);
+ when(realmConfig.getConfig(ALLOW_TABLE_LOCATION_OVERLAP, catalogEntity)).thenReturn(true);
+ when(realmConfig.getConfig(OPTIMIZED_SIBLING_CHECK, catalogEntity)).thenReturn(false);
+ when(realmConfig.getConfig(DEFAULT_UNIQUE_TABLE_LOCATION_ENABLED, catalogEntity))
+ .thenReturn(false);
+ catalog.initialize("catalog", Map.of("warehouse", "s3://bucket/base"));
+
+ String namespace = "n\u00e9\u6f22";
+ String table = "t\u00e9\u6f22";
+ String location = catalog.defaultWarehouseLocation(TableIdentifier.of(namespace, table));
+
+ String expectedSuffix =
+ String.format(
+ "/%s/%s/",
+ URLEncoder.encode(namespace, StandardCharsets.UTF_8),
+ URLEncoder.encode(table, StandardCharsets.UTF_8));
+ assertThat(location).endsWith(expectedSuffix);
+ }
+
@Test
void testFailedLocationSiblingResolutionException() {
when(resolver.resolveAll())