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

yufei 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 40283b25a Service: Add location tests for views (#2496)
40283b25a is described below

commit 40283b25a6cc348ca0797f5c024e405497e7d641
Author: Yufei Gu <[email protected]>
AuthorDate: Wed Sep 3 16:11:23 2025 -0700

    Service: Add location tests for views (#2496)
---
 .../iceberg/IcebergAllowedLocationTest.java        | 171 +++++++++++++++++++++
 1 file changed, 171 insertions(+)

diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
index f24d3dd37..789122dd8 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergAllowedLocationTest.java
@@ -20,28 +20,39 @@
 package org.apache.polaris.service.catalog.iceberg;
 
 import static 
org.apache.polaris.core.config.FeatureConfiguration.OPTIMIZED_SIBLING_CHECK;
+import static 
org.apache.polaris.core.entity.table.IcebergTableLikeEntity.USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY;
 import static org.apache.polaris.service.admin.PolarisAuthzTestBase.SCHEMA;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import jakarta.ws.rs.core.Response;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import org.apache.iceberg.MetadataUpdate;
 import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
+import org.apache.iceberg.view.ImmutableViewVersion;
 import org.apache.polaris.core.admin.model.Catalog;
 import org.apache.polaris.core.admin.model.CatalogProperties;
 import org.apache.polaris.core.admin.model.CreateCatalogRequest;
 import org.apache.polaris.core.admin.model.FileStorageConfigInfo;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
 import org.apache.polaris.service.TestServices;
+import org.jetbrains.annotations.NotNull;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
@@ -49,6 +60,17 @@ public class IcebergAllowedLocationTest {
   private static final String namespace = "ns";
   private static final String catalog = "test-catalog";
 
+  private static final String VIEW_QUERY = "select * from ns.tbl";
+  public static final ImmutableViewVersion VIEW_VERSION =
+      ImmutableViewVersion.builder()
+          .versionId(1)
+          .timestampMillis(System.currentTimeMillis())
+          .schemaId(1)
+          .defaultNamespace(Namespace.of(namespace))
+          .addRepresentations(
+              
ImmutableSQLViewRepresentation.builder().sql(VIEW_QUERY).dialect("spark").build())
+          .build();
+
   private String getTableName() {
     return "table_" + UUID.randomUUID();
   }
@@ -131,6 +153,155 @@ public class IcebergAllowedLocationTest {
     return services;
   }
 
+  @Test
+  void testViewWithAllowedLocations(@TempDir Path tmpDir) {
+    var viewId = TableIdentifier.of(namespace, "view");
+    var services = getTestServices();
+    var catalogLocation = 
tmpDir.resolve(catalog).toAbsolutePath().toUri().toString();
+    createCatalog(services, Map.of(), catalogLocation, 
List.of(catalogLocation));
+    var namespaceLocation = catalogLocation + "/" + namespace;
+    createNamespace(services, namespaceLocation);
+
+    // create a view with allowed locations
+    String customAllowedLocation1 = Paths.get(namespaceLocation, 
"custom-location1").toString();
+    String customAllowedLocation2 = Paths.get(namespaceLocation, 
"custom-location2").toString();
+
+    CreateViewRequest createViewRequest =
+        getCreateViewRequest(customAllowedLocation2, viewId.name(), 
customAllowedLocation1);
+    var response =
+        services
+            .restApi()
+            .createView(
+                catalog,
+                namespace,
+                createViewRequest,
+                services.realmContext(),
+                services.securityContext());
+
+    assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+
+    // update the view with allowed locations
+    String customAllowedLocation3 = Paths.get(namespaceLocation, 
"custom-location3").toString();
+
+    Map<String, String> updatedProperties = new HashMap<>();
+    updatedProperties.put(USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY, 
customAllowedLocation3);
+
+    UpdateTableRequest updateRequest =
+        UpdateTableRequest.create(
+            viewId, List.of(), List.of(new 
MetadataUpdate.SetProperties(updatedProperties)));
+
+    var updateResponse =
+        services
+            .catalogAdapter()
+            .newHandlerWrapper(services.securityContext(), catalog)
+            .replaceView(viewId, updateRequest);
+    assertEquals(
+        
updateResponse.metadata().properties().get(USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY),
+        customAllowedLocation3);
+  }
+
+  @Test
+  void testViewOutsideAllowedLocations(@TempDir Path tmpDir) {
+    var viewId = TableIdentifier.of(namespace, "view");
+    var services = getTestServices();
+
+    var catalogBaseLocation = 
tmpDir.resolve(catalog).toAbsolutePath().toUri().toString();
+    var namespaceLocation = catalogBaseLocation + "/" + namespace;
+
+    createCatalog(services, Map.of(), catalogBaseLocation, 
List.of(catalogBaseLocation));
+    createNamespace(services, namespaceLocation);
+
+    var locationNotAllowed =
+        
tmpDir.resolve("location-not-allowed").toAbsolutePath().toUri().toString();
+    var locationAllowed = Paths.get(namespaceLocation, 
"custom-location").toString();
+
+    // Test 1: Create a view with allowed location, and update it with a 
location not allowed
+    var properties = new HashMap<String, String>();
+
+    CreateViewRequest createViewRequest =
+        ImmutableCreateViewRequest.builder()
+            .name(viewId.name())
+            .schema(SCHEMA)
+            .viewVersion(VIEW_VERSION)
+            .location(locationAllowed)
+            .properties(properties)
+            .build();
+
+    var response =
+        services
+            .restApi()
+            .createView(
+                catalog,
+                namespace,
+                createViewRequest,
+                services.realmContext(),
+                services.securityContext());
+    assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+
+    Map<String, String> updatedProperties = new HashMap<>();
+    updatedProperties.put(USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY, 
locationNotAllowed);
+
+    var updateRequest =
+        UpdateTableRequest.create(
+            viewId,
+            List.of(), // requirements
+            List.of(new MetadataUpdate.SetProperties(updatedProperties)));
+
+    assertThatThrownBy(
+        () ->
+            services
+                .catalogAdapter()
+                .newHandlerWrapper(services.securityContext(), catalog)
+                .replaceView(viewId, updateRequest));
+
+    // Test 2: Try to create a view with location not allowed
+    var createViewRequestNotAllowed =
+        getCreateViewRequest(locationNotAllowed, "view2", locationNotAllowed);
+
+    assertThatThrownBy(
+            () ->
+                services
+                    .restApi()
+                    .createView(
+                        catalog,
+                        namespace,
+                        createViewRequestNotAllowed,
+                        services.realmContext(),
+                        services.securityContext()))
+        .isInstanceOf(ForbiddenException.class)
+        .hasMessageContaining("Invalid locations");
+
+    // Test 3: Try to create a view with metadata location not allowed
+    var createViewRequestMetadataNotAllowed =
+        getCreateViewRequest(locationNotAllowed, "view3", locationAllowed);
+
+    assertThatThrownBy(
+            () ->
+                services
+                    .restApi()
+                    .createView(
+                        catalog,
+                        namespace,
+                        createViewRequestMetadataNotAllowed,
+                        services.realmContext(),
+                        services.securityContext()))
+        .isInstanceOf(ForbiddenException.class)
+        .hasMessageContaining("Invalid locations");
+  }
+
+  private static @NotNull CreateViewRequest getCreateViewRequest(
+      String writeMetadataPath, String viewName, String location) {
+    var properties = new HashMap<String, String>();
+    properties.put(USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY, 
writeMetadataPath);
+    return ImmutableCreateViewRequest.builder()
+        .name(viewName)
+        .schema(SCHEMA)
+        .viewVersion(VIEW_VERSION)
+        .location(location)
+        .properties(properties)
+        .build();
+  }
+
   private void createCatalog(
       TestServices services,
       Map<String, String> catalogConfig,

Reply via email to