This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 0bb6f936da [#9745]feat(core,IRC): Add view entity and management layer
(#9787)
0bb6f936da is described below
commit 0bb6f936da06b8dda8d9b6b674ed89fafc2f6b5e
Author: Bharath Krishna <[email protected]>
AuthorDate: Wed Feb 4 18:43:41 2026 -0800
[#9745]feat(core,IRC): Add view entity and management layer (#9787)
### What changes were proposed in this pull request?
Adds view entity and management layer to Gravitino core and Iceberg REST
catalog (IRC), including:
- New View and ViewCatalog interfaces in the API
- ViewManager for internal view operations
- View support in Iceberg catalog operations (loadView, viewExists)
- View hook dispatcher and event handling
### Why are the changes needed?
To enable view support in Gravitino, providing the foundational layer
for managing database views alongside tables. This is a prerequisite for
exposing view operations through Gravitino APIs.
Fix: #9745
### Does this PR introduce _any_ user-facing change?
No. This is internal infrastructure only. Full view operations (create,
list, alter, drop) and view authorization may be added in future PRs
### How was this patch tested?
Added unit tests for view manager, view operations, and Iceberg catalog
view support.
---
.../main/java/org/apache/gravitino/Catalog.java | 9 ++
.../gravitino/exceptions/NoSuchViewException.java | 49 ++++++
.../main/java/org/apache/gravitino/rel/View.java | 54 +++++++
.../java/org/apache/gravitino/rel/ViewCatalog.java | 57 +++++++
.../catalog/lakehouse/iceberg/IcebergCatalog.java | 6 +
.../iceberg/IcebergCatalogOperations.java | 30 +++-
.../catalog/lakehouse/iceberg/IcebergView.java | 108 +++++++++++++
.../lakehouse/iceberg/TestIcebergCatalog.java | 25 +++
.../catalog/lakehouse/iceberg/TestIcebergView.java | 110 +++++++++++++
.../java/org/apache/gravitino/GravitinoEnv.java | 21 ++-
.../apache/gravitino/catalog/CatalogManager.java | 15 ++
.../apache/gravitino/catalog/ViewDispatcher.java | 29 ++++
.../gravitino/catalog/ViewOperationDispatcher.java | 80 ++++++++++
.../apache/gravitino/utils/MetadataObjectUtil.java | 5 +
.../catalog/TestViewOperationDispatcher.java | 175 ++++++++++++++++++++
.../gravitino/connector/TestCatalogOperations.java | 17 ++
.../org/apache/gravitino/iceberg/RESTService.java | 5 +-
.../service/dispatcher/IcebergOwnershipUtils.java | 34 +++-
.../dispatcher/IcebergViewHookDispatcher.java | 154 ++++++++++++++++++
.../dispatcher/TestIcebergOwnershipUtils.java | 34 +++-
.../TestIcebergViewOperationExecutor.java | 176 +++++++++++++++++++++
21 files changed, 1185 insertions(+), 8 deletions(-)
diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java
b/api/src/main/java/org/apache/gravitino/Catalog.java
index ed63285e95..9880f8d8d0 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -29,6 +29,7 @@ import org.apache.gravitino.messaging.TopicCatalog;
import org.apache.gravitino.model.ModelCatalog;
import org.apache.gravitino.policy.SupportsPolicies;
import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.tag.SupportsTags;
/**
@@ -246,6 +247,14 @@ public interface Catalog extends Auditable {
throw new UnsupportedOperationException("Catalog does not support function
operations");
}
+ /**
+ * @return the {@link ViewCatalog} if the catalog supports view operations.
+ * @throws UnsupportedOperationException if the catalog does not support
view operations.
+ */
+ default ViewCatalog asViewCatalog() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Catalog does not support view
operations");
+ }
+
/**
* @return the {@link SupportsTags} if the catalog supports tag operations.
* @throws UnsupportedOperationException if the catalog does not support tag
operations.
diff --git
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java
new file mode 100644
index 0000000000..15cc7b0671
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchViewException.java
@@ -0,0 +1,49 @@
+/*
+ * 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.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a view with specified name does not exist. */
+public class NoSuchViewException extends NotFoundException {
+
+ /**
+ * Constructs a NoSuchViewException.
+ *
+ * @param message The error message.
+ * @param args Additional arguments for formatting the error message.
+ */
+ @FormatMethod
+ public NoSuchViewException(@FormatString String message, Object... args) {
+ super(message, args);
+ }
+
+ /**
+ * Constructs a NoSuchViewException with a cause.
+ *
+ * @param cause The cause of the exception.
+ * @param message The error message.
+ * @param args Additional arguments for formatting the error message.
+ */
+ @FormatMethod
+ public NoSuchViewException(Throwable cause, String message, Object... args) {
+ super(cause, message, args);
+ }
+}
diff --git a/api/src/main/java/org/apache/gravitino/rel/View.java
b/api/src/main/java/org/apache/gravitino/rel/View.java
new file mode 100644
index 0000000000..1d425e7494
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/rel/View.java
@@ -0,0 +1,54 @@
+/*
+ * 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.gravitino.rel;
+
+import java.util.Collections;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.Unstable;
+
+/**
+ * An interface representing a view in a {@link Namespace}. It defines the
basic properties of a
+ * view. A catalog implementation with {@link ViewCatalog} should implement
this interface.
+ */
+@Unstable
+public interface View extends Auditable {
+
+ /**
+ * @return The name of the view.
+ */
+ String name();
+
+ /**
+ * @return The comment of the view, null if no comment is set.
+ */
+ @Nullable
+ default String comment() {
+ return null;
+ }
+
+ /**
+ * @return The properties of the view, empty map if no properties are set.
+ */
+ default Map<String, String> properties() {
+ return Collections.emptyMap();
+ }
+}
diff --git a/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java
b/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java
new file mode 100644
index 0000000000..2bf423f97b
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/rel/ViewCatalog.java
@@ -0,0 +1,57 @@
+/*
+ * 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.gravitino.rel;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.Unstable;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+
+/**
+ * The ViewCatalog interface defines the public API for managing views in a
schema. If the catalog
+ * implementation supports views, it must implement this interface.
+ *
+ * <p>Note: This is a minimal interface. Full operations (create, list, alter,
drop) will be added
+ * when Gravitino APIs support views.
+ */
+@Unstable
+public interface ViewCatalog {
+
+ /**
+ * Load view metadata by {@link NameIdentifier} from the catalog.
+ *
+ * @param ident A view identifier.
+ * @return The view metadata.
+ * @throws NoSuchViewException If the view does not exist.
+ */
+ View loadView(NameIdentifier ident) throws NoSuchViewException;
+
+ /**
+ * Check if a view exists using its identifier.
+ *
+ * @param ident A view identifier.
+ * @return true If the view exists, false otherwise.
+ */
+ default boolean viewExists(NameIdentifier ident) {
+ try {
+ return loadView(ident) != null;
+ } catch (NoSuchViewException e) {
+ return false;
+ }
+ }
+}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
index 58858ec81b..2e838e2be5 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalog.java
@@ -23,6 +23,7 @@ import org.apache.gravitino.connector.BaseCatalog;
import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.connector.capability.Capability;
+import org.apache.gravitino.rel.ViewCatalog;
/** Implementation of an Apache Iceberg catalog in Apache Gravitino. */
public class IcebergCatalog extends BaseCatalog<IcebergCatalog> {
@@ -56,6 +57,11 @@ public class IcebergCatalog extends
BaseCatalog<IcebergCatalog> {
return ops;
}
+ @Override
+ public ViewCatalog asViewCatalog() {
+ return (ViewCatalog) ops();
+ }
+
@Override
public Capability newCapability() {
return new IcebergCatalogCapability();
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
index 64a951cb81..99f143e8ad 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergCatalogOperations.java
@@ -46,6 +46,7 @@ import
org.apache.gravitino.exceptions.ConnectionFailedException;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
@@ -60,6 +61,8 @@ import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.rel.expressions.distributions.Distribution;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
import org.apache.gravitino.rel.expressions.sorts.SortOrder;
@@ -77,12 +80,14 @@ import
org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
import org.apache.iceberg.rest.responses.GetNamespaceResponse;
import org.apache.iceberg.rest.responses.ListTablesResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Operations for interacting with an Apache Iceberg catalog in Apache
Gravitino. */
-public class IcebergCatalogOperations implements CatalogOperations,
SupportsSchemas, TableCatalog {
+public class IcebergCatalogOperations
+ implements CatalogOperations, SupportsSchemas, TableCatalog, ViewCatalog {
private static final String ICEBERG_TABLE_DOES_NOT_EXIST_MSG = "Iceberg
table does not exist: %s";
@@ -620,6 +625,29 @@ public class IcebergCatalogOperations implements
CatalogOperations, SupportsSche
}
}
+ /**
+ * Load view metadata from the Iceberg catalog.
+ *
+ * <p>Delegates to the underlying Iceberg REST catalog to load view metadata.
+ *
+ * @param ident The identifier of the view to load.
+ * @return The loaded view metadata.
+ * @throws NoSuchViewException If the view does not exist.
+ */
+ @Override
+ public View loadView(NameIdentifier ident) throws NoSuchViewException {
+ try {
+ LoadViewResponse response =
+ icebergCatalogWrapper.loadView(
+ IcebergCatalogWrapperHelper.buildIcebergTableIdentifier(ident));
+
+ return IcebergView.fromLoadViewResponse(response, ident.name());
+ } catch (Exception e) {
+ throw new NoSuchViewException(
+ e, "Failed to load view %s from Iceberg catalog: %s", ident,
e.getMessage());
+ }
+ }
+
private static Distribution getIcebergDefaultDistribution(
boolean isSorted, boolean isPartitioned) {
if (isSorted) {
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
new file mode 100644
index 0000000000..73894d4745
--- /dev/null
+++
b/catalogs/catalog-lakehouse-iceberg/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergView.java
@@ -0,0 +1,108 @@
+/*
+ * 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.gravitino.catalog.lakehouse.iceberg;
+
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Map;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rel.View;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+
+/** Represents an Apache Iceberg View entity in the Iceberg catalog. */
+@ToString
+@Getter
+public class IcebergView implements View {
+
+ private String name;
+ private Map<String, String> properties;
+ private AuditInfo auditInfo;
+
+ private IcebergView() {}
+
+ /**
+ * Converts an Iceberg LoadViewResponse to a Gravitino IcebergView.
+ *
+ * @param response The Iceberg LoadViewResponse.
+ * @param viewName The name of the view.
+ * @return A new IcebergView instance.
+ */
+ public static IcebergView fromLoadViewResponse(LoadViewResponse response,
String viewName) {
+ Map<String, String> properties =
+ response.metadata() != null && response.metadata().properties() != null
+ ? Maps.newHashMap(response.metadata().properties())
+ : Maps.newHashMap();
+
+ return IcebergView.builder()
+ .withName(viewName)
+ .withProperties(properties)
+ .withAuditInfo(AuditInfo.EMPTY)
+ .build();
+ }
+
+ @Override
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return properties != null ? properties : Collections.emptyMap();
+ }
+
+ @Override
+ public AuditInfo auditInfo() {
+ return auditInfo;
+ }
+
+ /** Returns a new builder for constructing an IcebergView. */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /** Builder for IcebergView. */
+ public static class Builder {
+ private final IcebergView view;
+
+ private Builder() {
+ this.view = new IcebergView();
+ }
+
+ public Builder withName(String name) {
+ view.name = name;
+ return this;
+ }
+
+ public Builder withProperties(Map<String, String> properties) {
+ view.properties = properties != null ? Maps.newHashMap(properties) :
Maps.newHashMap();
+ return this;
+ }
+
+ public Builder withAuditInfo(AuditInfo auditInfo) {
+ view.auditInfo = auditInfo;
+ return this;
+ }
+
+ public IcebergView build() {
+ return view;
+ }
+ }
+}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
index acd3c3a1f0..e6e2f20741 100644
---
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergCatalog.java
@@ -33,6 +33,7 @@ import org.apache.gravitino.connector.PropertiesMetadata;
import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.CatalogEntity;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.iceberg.rest.responses.ListNamespacesResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -199,4 +200,28 @@ public class TestIcebergCatalog {
throwable.getMessage().contains(IcebergCatalogPropertiesMetadata.CATALOG_BACKEND));
}
}
+
+ @Test
+ public void testAsViewCatalog() {
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build();
+
+ CatalogEntity entity =
+ CatalogEntity.builder()
+ .withId(1L)
+ .withName("catalog")
+ .withNamespace(Namespace.of("metalake"))
+ .withType(IcebergCatalog.Type.RELATIONAL)
+ .withProvider("iceberg")
+ .withAuditInfo(auditInfo)
+ .build();
+
+ Map<String, String> conf = Maps.newHashMap();
+ IcebergCatalog icebergCatalog =
+ new IcebergCatalog().withCatalogConf(conf).withCatalogEntity(entity);
+
+ ViewCatalog viewCatalog = icebergCatalog.asViewCatalog();
+ Assertions.assertNotNull(viewCatalog);
+ Assertions.assertTrue(viewCatalog instanceof IcebergCatalogOperations);
+ }
}
diff --git
a/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
new file mode 100644
index 0000000000..cfc9563663
--- /dev/null
+++
b/catalogs/catalog-lakehouse-iceberg/src/test/java/org/apache/gravitino/catalog/lakehouse/iceberg/TestIcebergView.java
@@ -0,0 +1,110 @@
+/*
+ * 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.gravitino.catalog.lakehouse.iceberg;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.apache.iceberg.view.ViewMetadata;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergView {
+
+ @Test
+ public void testFromLoadViewResponse() {
+ // Test with properties
+ Map<String, String> properties = ImmutableMap.of("key1", "value1", "key2",
"value2");
+ ViewMetadata mockMetadata = mock(ViewMetadata.class);
+ when(mockMetadata.properties()).thenReturn(properties);
+
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockResponse.metadata()).thenReturn(mockMetadata);
+
+ IcebergView view = IcebergView.fromLoadViewResponse(mockResponse,
"test_view");
+
+ Assertions.assertEquals("test_view", view.name());
+ Assertions.assertEquals(properties, view.properties());
+ Assertions.assertEquals(AuditInfo.EMPTY, view.auditInfo());
+ }
+
+ @Test
+ public void testFromLoadViewResponseWithNullProperties() {
+ // Test with null metadata
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockResponse.metadata()).thenReturn(null);
+
+ IcebergView view = IcebergView.fromLoadViewResponse(mockResponse,
"test_view_null");
+
+ Assertions.assertEquals("test_view_null", view.name());
+ Assertions.assertNotNull(view.properties());
+ Assertions.assertTrue(view.properties().isEmpty());
+ Assertions.assertNotNull(view.auditInfo());
+ }
+
+ @Test
+ public void testFromLoadViewResponseWithNullMetadataProperties() {
+ // Test with null properties in metadata
+ ViewMetadata mockMetadata = mock(ViewMetadata.class);
+ when(mockMetadata.properties()).thenReturn(null);
+
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockResponse.metadata()).thenReturn(mockMetadata);
+
+ IcebergView view = IcebergView.fromLoadViewResponse(mockResponse,
"test_view_empty");
+
+ Assertions.assertEquals("test_view_empty", view.name());
+ Assertions.assertNotNull(view.properties());
+ Assertions.assertTrue(view.properties().isEmpty());
+ }
+
+ @Test
+ public void testBuilder() {
+ Map<String, String> properties = ImmutableMap.of("prop1", "val1");
+ AuditInfo auditInfo =
AuditInfo.builder().withCreator("test_user").withCreateTime(null).build();
+
+ IcebergView view =
+ IcebergView.builder()
+ .withName("builder_view")
+ .withProperties(properties)
+ .withAuditInfo(auditInfo)
+ .build();
+
+ Assertions.assertEquals("builder_view", view.name());
+ Assertions.assertEquals(properties, view.properties());
+ Assertions.assertEquals(auditInfo, view.auditInfo());
+ Assertions.assertEquals("test_user", view.auditInfo().creator());
+ }
+
+ @Test
+ public void testBuilderWithNullProperties() {
+ AuditInfo auditInfo =
AuditInfo.builder().withCreator("test_user").withCreateTime(null).build();
+
+ IcebergView view =
+
IcebergView.builder().withName("view_null_props").withAuditInfo(auditInfo).build();
+
+ Assertions.assertEquals("view_null_props", view.name());
+ Assertions.assertNotNull(view.properties());
+ Assertions.assertTrue(view.properties().isEmpty());
+ }
+}
diff --git a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
index 6fba49b808..b0a58554d4 100644
--- a/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
+++ b/core/src/main/java/org/apache/gravitino/GravitinoEnv.java
@@ -52,6 +52,8 @@ import org.apache.gravitino.catalog.TableOperationDispatcher;
import org.apache.gravitino.catalog.TopicDispatcher;
import org.apache.gravitino.catalog.TopicNormalizeDispatcher;
import org.apache.gravitino.catalog.TopicOperationDispatcher;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.catalog.ViewOperationDispatcher;
import org.apache.gravitino.credential.CredentialOperationDispatcher;
import org.apache.gravitino.hook.AccessControlHookDispatcher;
import org.apache.gravitino.hook.CatalogHookDispatcher;
@@ -133,6 +135,8 @@ public class GravitinoEnv {
private FunctionDispatcher functionDispatcher;
+ private ViewDispatcher viewDispatcher;
+
private MetalakeDispatcher metalakeDispatcher;
private CredentialOperationDispatcher credentialOperationDispatcher;
@@ -270,7 +274,16 @@ public class GravitinoEnv {
}
/**
- * Get the PartitionDispatcher associated with the Gravitino environment.
+ * Get the ViewDispatcher associated with the Gravitino environment.
+ *
+ * @return The ViewDispatcher instance.
+ */
+ public ViewDispatcher viewDispatcher() {
+ return viewDispatcher;
+ }
+
+ /**
+ * * Get the PartitionDispatcher associated with the Gravitino environment.
*
* @return The PartitionDispatcher instance.
*/
@@ -609,6 +622,12 @@ public class GravitinoEnv {
this.functionDispatcher =
new FunctionNormalizeDispatcher(functionOperationDispatcher,
catalogManager);
+ // TODO: Add ViewHookDispatcher and ViewEventDispatcher when needed for
view-specific hooks
+ // and event handling.
+ ViewOperationDispatcher viewOperationDispatcher =
+ new ViewOperationDispatcher(catalogManager, entityStore, idGenerator);
+ this.viewDispatcher = viewOperationDispatcher;
+
this.statisticDispatcher =
new StatisticEventDispatcher(
eventBus, new StatisticManager(entityStore, idGenerator, config));
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
index b9c21fcb1f..abc0b42517 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CatalogManager.java
@@ -98,6 +98,7 @@ import org.apache.gravitino.model.ModelCatalog;
import org.apache.gravitino.rel.SupportsPartitions;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.storage.IdGenerator;
import org.apache.gravitino.utils.IsolatedClassLoader;
import org.apache.gravitino.utils.NamespaceUtil;
@@ -148,6 +149,16 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
});
}
+ public <R> R doWithViewOps(ThrowableFunction<ViewCatalog, R> fn) throws
Exception {
+ return classLoader.withClassLoader(
+ cl -> {
+ if (asViews() == null) {
+ throw new UnsupportedOperationException("Catalog does not
support view operations");
+ }
+ return fn.apply(asViews());
+ });
+ }
+
public <R> R doWithFilesetOps(ThrowableFunction<FilesetCatalog, R> fn)
throws Exception {
return classLoader.withClassLoader(
cl -> {
@@ -246,6 +257,10 @@ public class CatalogManager implements CatalogDispatcher,
Closeable {
return catalog.ops() instanceof TableCatalog ? (TableCatalog)
catalog.ops() : null;
}
+ private ViewCatalog asViews() {
+ return catalog.ops() instanceof ViewCatalog ? (ViewCatalog)
catalog.ops() : null;
+ }
+
private FilesetCatalog asFilesets() {
return catalog.ops() instanceof FilesetCatalog ? (FilesetCatalog)
catalog.ops() : null;
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java
new file mode 100644
index 0000000000..2961ca7890
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/catalog/ViewDispatcher.java
@@ -0,0 +1,29 @@
+/*
+ * 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.gravitino.catalog;
+
+import org.apache.gravitino.rel.ViewCatalog;
+
+/**
+ * {@code ViewDispatcher} interface acts as a specialization of the {@link
ViewCatalog} interface.
+ * This interface is designed to potentially add custom behaviors or
operations related to
+ * dispatching or handling view-related events or actions that are not covered
by the standard
+ * {@code ViewCatalog} operations.
+ */
+public interface ViewDispatcher extends ViewCatalog {}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
new file mode 100644
index 0000000000..c49a93abfb
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/catalog/ViewOperationDispatcher.java
@@ -0,0 +1,80 @@
+/*
+ * 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.gravitino.catalog;
+
+import static
org.apache.gravitino.utils.NameIdentifierUtil.getCatalogIdentifier;
+
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.lock.LockType;
+import org.apache.gravitino.lock.TreeLockUtils;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.storage.IdGenerator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code ViewOperationDispatcher} is the operation dispatcher for view
operations.
+ *
+ * <p>Currently only supports loadView(). Full CRUD operations (create, alter,
drop) needs to be
+ * added when Gravitino APIs support view management.
+ */
+public class ViewOperationDispatcher extends OperationDispatcher implements
ViewDispatcher {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ViewOperationDispatcher.class);
+
+ /**
+ * Creates a new ViewOperationDispatcher instance.
+ *
+ * @param catalogManager The CatalogManager instance to be used for view
operations.
+ * @param store The EntityStore instance to be used for view operations.
+ * @param idGenerator The IdGenerator instance to be used for view
operations.
+ */
+ public ViewOperationDispatcher(
+ CatalogManager catalogManager, EntityStore store, IdGenerator
idGenerator) {
+ super(catalogManager, store, idGenerator);
+ }
+
+ /**
+ * Load view metadata by identifier from the catalog.
+ *
+ * <p>Delegates directly to the underlying catalog's ViewCatalog interface.
Views are loaded from
+ * the external catalog without caching in Gravitino's EntityStore.
+ *
+ * <p>TODO(#9746): Add entity storage support to cache view metadata in
EntityStore.
+ *
+ * @param ident The view identifier.
+ * @return The loaded view metadata.
+ * @throws NoSuchViewException If the view does not exist.
+ */
+ @Override
+ public View loadView(NameIdentifier ident) throws NoSuchViewException {
+ LOG.info("Loading view: {}", ident);
+
+ return TreeLockUtils.doWithTreeLock(
+ ident,
+ LockType.READ,
+ () ->
+ doWithCatalog(
+ getCatalogIdentifier(ident),
+ c -> c.doWithViewOps(v -> v.loadView(ident)),
+ NoSuchViewException.class));
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
index 0b0cb18760..8b075bff1f 100644
--- a/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/MetadataObjectUtil.java
@@ -201,6 +201,11 @@ public class MetadataObjectUtil {
check(env.modelDispatcher().modelExists(identifier),
exceptionToThrowSupplier);
break;
+ case VIEW:
+ NameIdentifierUtil.checkView(identifier);
+ check(env.viewDispatcher().viewExists(identifier),
exceptionToThrowSupplier);
+ break;
+
case ROLE:
AuthorizationUtils.checkRole(identifier);
try {
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
new file mode 100644
index 0000000000..f602a2b53b
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestViewOperationDispatcher.java
@@ -0,0 +1,175 @@
+/*
+ * 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.gravitino.catalog;
+
+import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
+import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Map;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.TestCatalog;
+import org.apache.gravitino.connector.TestCatalogOperations;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.lock.LockManager;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rel.View;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+public class TestViewOperationDispatcher extends TestOperationDispatcher {
+ static ViewOperationDispatcher viewOperationDispatcher;
+ static SchemaOperationDispatcher schemaOperationDispatcher;
+
+ @BeforeAll
+ public static void initialize() throws IOException, IllegalAccessException {
+ schemaOperationDispatcher =
+ new SchemaOperationDispatcher(catalogManager, entityStore,
idGenerator);
+ viewOperationDispatcher = new ViewOperationDispatcher(catalogManager,
entityStore, idGenerator);
+
+ Config config = mock(Config.class);
+ doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
+ doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
+ doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new
LockManager(config), true);
+ FieldUtils.writeField(
+ GravitinoEnv.getInstance(), "schemaDispatcher",
schemaOperationDispatcher, true);
+ }
+
+ public static ViewOperationDispatcher getViewOperationDispatcher() {
+ return viewOperationDispatcher;
+ }
+
+ public static SchemaOperationDispatcher getSchemaOperationDispatcher() {
+ return schemaOperationDispatcher;
+ }
+
+ public static CatalogManager getCatalogManager() {
+ return catalogManager;
+ }
+
+ @Test
+ public void testLoadView() throws IOException {
+ Namespace viewNs = Namespace.of(metalake, catalog, "schema61");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ schemaOperationDispatcher.createSchema(NameIdentifier.of(viewNs.levels()),
"comment", props);
+
+ NameIdentifier viewIdent1 = NameIdentifier.of(viewNs, "view1");
+
+ // Create a mock view through the catalog operations
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+ View mockView =
+ new View() {
+ @Override
+ public String name() {
+ return "view1";
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return props;
+ }
+
+ @Override
+ public AuditInfo auditInfo() {
+ return auditInfo;
+ }
+ };
+
+ // Mock the catalog operations to return the view
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+ testCatalogOperations.views.put(viewIdent1, mockView);
+
+ // Test load view
+ View loadedView = viewOperationDispatcher.loadView(viewIdent1);
+ Assertions.assertEquals("view1", loadedView.name());
+ Assertions.assertEquals("test", loadedView.auditInfo().creator());
+
+ // Test load non-existent view
+ NameIdentifier viewIdent2 = NameIdentifier.of(viewNs, "non_existent_view");
+ Assertions.assertThrows(
+ NoSuchViewException.class, () ->
viewOperationDispatcher.loadView(viewIdent2));
+ }
+
+ @Test
+ public void testLoadViewWithInvalidNamespace() {
+ Namespace invalidNs = Namespace.of(metalake, catalog,
"non_existent_schema");
+ NameIdentifier viewIdent = NameIdentifier.of(invalidNs, "view1");
+
+ Assertions.assertThrows(
+ NoSuchViewException.class, () ->
viewOperationDispatcher.loadView(viewIdent));
+ }
+
+ @Test
+ public void testLoadViewWithMultipleViews() throws IOException {
+ Namespace viewNs = Namespace.of(metalake, catalog, "schema62");
+ Map<String, String> props = ImmutableMap.of("k1", "v1", "k2", "v2");
+ schemaOperationDispatcher.createSchema(NameIdentifier.of(viewNs.levels()),
"comment", props);
+
+ // Create multiple views
+ TestCatalog testCatalog =
+ (TestCatalog) catalogManager.loadCatalog(NameIdentifier.of(metalake,
catalog));
+ TestCatalogOperations testCatalogOperations = (TestCatalogOperations)
testCatalog.ops();
+
+ for (int i = 1; i <= 3; i++) {
+ NameIdentifier viewIdent = NameIdentifier.of(viewNs, "view" + i);
+ AuditInfo auditInfo =
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build();
+ int index = i;
+ View mockView =
+ new View() {
+ @Override
+ public String name() {
+ return "view" + index;
+ }
+
+ @Override
+ public Map<String, String> properties() {
+ return props;
+ }
+
+ @Override
+ public AuditInfo auditInfo() {
+ return auditInfo;
+ }
+ };
+ testCatalogOperations.views.put(viewIdent, mockView);
+ }
+
+ // Test loading each view
+ for (int i = 1; i <= 3; i++) {
+ NameIdentifier viewIdent = NameIdentifier.of(viewNs, "view" + i);
+ View loadedView = viewOperationDispatcher.loadView(viewIdent);
+ Assertions.assertEquals("view" + i, loadedView.name());
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
index 10b6bc123c..78658035e9 100644
---
a/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
+++
b/core/src/test/java/org/apache/gravitino/connector/TestCatalogOperations.java
@@ -70,6 +70,7 @@ import
org.apache.gravitino.exceptions.NoSuchModelVersionURINameException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.exceptions.NoSuchTopicException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
@@ -91,6 +92,8 @@ import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.rel.expressions.distributions.Distribution;
import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.expressions.transforms.Transform;
@@ -101,6 +104,7 @@ import org.slf4j.LoggerFactory;
public class TestCatalogOperations
implements CatalogOperations,
TableCatalog,
+ ViewCatalog,
FilesetCatalog,
TopicCatalog,
ModelCatalog,
@@ -121,6 +125,8 @@ public class TestCatalogOperations
private final Map<Pair<NameIdentifier, String>, Integer> modelAliasToVersion;
+ public final Map<NameIdentifier, View> views;
+
public static final String FAIL_CREATE = "fail-create";
public static final String FAIL_TEST = "need-fail";
@@ -135,6 +141,7 @@ public class TestCatalogOperations
models = Maps.newHashMap();
modelVersions = Maps.newHashMap();
modelAliasToVersion = Maps.newHashMap();
+ views = Maps.newHashMap();
}
@Override
@@ -295,6 +302,16 @@ public class TestCatalogOperations
}
}
+ // ViewCatalog methods
+ @Override
+ public View loadView(NameIdentifier ident) throws NoSuchViewException {
+ if (views.containsKey(ident)) {
+ return views.get(ident);
+ } else {
+ throw new NoSuchViewException("View %s does not exist", ident);
+ }
+ }
+
@Override
public NameIdentifier[] listSchemas(Namespace namespace) throws
NoSuchCatalogException {
return schemas.keySet().stream()
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
index 39ac49b5ff..553b803edb 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/RESTService.java
@@ -40,6 +40,7 @@ import
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableHookDispatche
import
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableOperationDispatcher;
import
org.apache.gravitino.iceberg.service.dispatcher.IcebergTableOperationExecutor;
import
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewEventDispatcher;
+import
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewHookDispatcher;
import
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewOperationDispatcher;
import
org.apache.gravitino.iceberg.service.dispatcher.IcebergViewOperationExecutor;
import org.apache.gravitino.iceberg.service.metrics.IcebergMetricsManager;
@@ -114,8 +115,10 @@ public class RESTService implements
GravitinoAuxiliaryService {
new IcebergTableEventDispatcher(icebergTableOperationDispatcher,
eventBus, metalakeName);
IcebergViewOperationExecutor icebergViewOperationExecutor =
new IcebergViewOperationExecutor(icebergCatalogWrapperManager);
+ IcebergViewHookDispatcher icebergViewHookDispatcher =
+ new IcebergViewHookDispatcher(icebergViewOperationExecutor,
metalakeName);
IcebergViewEventDispatcher icebergViewEventDispatcher =
- new IcebergViewEventDispatcher(icebergViewOperationExecutor, eventBus,
metalakeName);
+ new IcebergViewEventDispatcher(icebergViewHookDispatcher, eventBus,
metalakeName);
IcebergNamespaceOperationDispatcher namespaceOperationDispatcher =
new IcebergNamespaceOperationExecutor(icebergCatalogWrapperManager);
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
index b17cdf25cd..1d30592593 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergOwnershipUtils.java
@@ -27,8 +27,9 @@ import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
/**
- * Utility class for managing ownership of Iceberg resources (schemas and
tables) in Gravitino.
- * Provides centralized methods for setting ownership during resource creation
operations.
+ * Utility class for managing ownership of Iceberg resources (schemas, tables,
and views) in
+ * Gravitino. Provides centralized methods for setting ownership during
resource creation
+ * operations.
*/
public class IcebergOwnershipUtils {
@@ -87,4 +88,33 @@ public class IcebergOwnershipUtils {
Owner.Type.USER);
}
}
+
+ /**
+ * Sets the owner of a view to the specified user using the provided owner
dispatcher.
+ *
+ * @param metalake the metalake name
+ * @param catalogName the catalog name
+ * @param namespace the Iceberg namespace containing the view
+ * @param viewName the view name
+ * @param user the user to set as owner
+ * @param ownerDispatcher the owner dispatcher to use
+ */
+ public static void setViewOwner(
+ String metalake,
+ String catalogName,
+ Namespace namespace,
+ String viewName,
+ String user,
+ OwnerDispatcher ownerDispatcher) {
+ if (ownerDispatcher != null) {
+ ownerDispatcher.setOwner(
+ metalake,
+ NameIdentifierUtil.toMetadataObject(
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(
+ metalake, catalogName, TableIdentifier.of(namespace,
viewName)),
+ Entity.EntityType.VIEW),
+ user,
+ Owner.Type.USER);
+ }
+ }
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
new file mode 100644
index 0000000000..b686346ce3
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergViewHookDispatcher.java
@@ -0,0 +1,154 @@
+/*
+ * 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.gravitino.iceberg.service.dispatcher;
+
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.iceberg.common.utils.IcebergIdentifierUtils;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code IcebergViewHookDispatcher} is a decorator for {@link
IcebergViewOperationDispatcher} that
+ * imports views into Gravitino's metadata catalog when they are created or
accessed via Iceberg
+ * REST.
+ *
+ * <p>This is the key integration point between Iceberg REST views and
Gravitino's view management
+ * system. When a view is created or loaded through Iceberg REST, this
dispatcher ensures that
+ * Gravitino is aware of the view by calling {@link ViewDispatcher#loadView}.
+ */
+public class IcebergViewHookDispatcher implements
IcebergViewOperationDispatcher {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(IcebergViewHookDispatcher.class);
+
+ private final IcebergViewOperationDispatcher dispatcher;
+ private final String metalake;
+
+ public IcebergViewHookDispatcher(IcebergViewOperationDispatcher dispatcher,
String metalake) {
+ this.dispatcher = dispatcher;
+ this.metalake = metalake;
+ }
+
+ @Override
+ public LoadViewResponse createView(
+ IcebergRequestContext context, Namespace namespace, CreateViewRequest
createViewRequest) {
+ // First, create the view in the underlying catalog
+ LoadViewResponse response = dispatcher.createView(context, namespace,
createViewRequest);
+
+ // Then import it into Gravitino so Gravitino is aware of the view
+ importView(context.catalogName(), namespace, createViewRequest.name());
+
+ // TODO(#9746): Enable view ownership once ViewMetaService is implemented
+ // Currently disabled because VIEW entity type is not supported in
+ // RelationalEntityStoreIdResolver
+ // IcebergOwnershipUtils.setViewOwner(
+ // metalake,
+ // context.catalogName(),
+ // namespace,
+ // createViewRequest.name(),
+ // context.userName(),
+ // GravitinoEnv.getInstance().ownerDispatcher());
+
+ return response;
+ }
+
+ @Override
+ public LoadViewResponse loadView(IcebergRequestContext context,
TableIdentifier viewIdentifier) {
+ return dispatcher.loadView(context, viewIdentifier);
+ }
+
+ @Override
+ public LoadViewResponse replaceView(
+ IcebergRequestContext context,
+ TableIdentifier viewIdentifier,
+ UpdateTableRequest replaceViewRequest) {
+ return dispatcher.replaceView(context, viewIdentifier, replaceViewRequest);
+ }
+
+ @Override
+ public void dropView(IcebergRequestContext context, TableIdentifier
viewIdentifier) {
+ dispatcher.dropView(context, viewIdentifier);
+ // Note: We don't remove from Gravitino here as that will be handled by
entity storage
+ // in future work (issue #9746)
+ }
+
+ @Override
+ public ListTablesResponse listView(IcebergRequestContext context, Namespace
namespace) {
+ return dispatcher.listView(context, namespace);
+ }
+
+ @Override
+ public boolean viewExists(IcebergRequestContext context, TableIdentifier
viewIdentifier) {
+ return dispatcher.viewExists(context, viewIdentifier);
+ }
+
+ @Override
+ public void renameView(IcebergRequestContext context, RenameTableRequest
renameViewRequest) {
+ dispatcher.renameView(context, renameViewRequest);
+ // Note: Rename handling in Gravitino will be added with full view support
(issue #9746)
+ }
+
+ /**
+ * Import a view into Gravitino's metadata catalog by loading it through the
ViewDispatcher.
+ *
+ * <p>This method calls {@link ViewDispatcher#loadView} which delegates to
the underlying
+ * catalog's ViewCatalog implementation. This ensures Gravitino is aware of
the view and can apply
+ * authorization policies to it.
+ *
+ * <p>This is a best-effort operation - if it fails, we log a warning but
don't fail the Iceberg
+ * REST operation, as the view was successfully created in the underlying
catalog.
+ *
+ * @param catalogName The name of the Gravitino catalog.
+ * @param namespace The Iceberg namespace containing the view.
+ * @param viewName The name of the view.
+ */
+ private void importView(String catalogName, Namespace namespace, String
viewName) {
+ ViewDispatcher viewDispatcher =
GravitinoEnv.getInstance().viewDispatcher();
+ if (viewDispatcher != null) {
+ try {
+ viewDispatcher.loadView(
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(
+ metalake, catalogName, TableIdentifier.of(namespace,
viewName)));
+ LOG.info(
+ "Successfully imported view into Gravitino: {}.{}.{}.{}",
+ metalake,
+ catalogName,
+ namespace,
+ viewName);
+ } catch (Exception e) {
+ // Log but don't fail - view import is best-effort
+ LOG.warn(
+ "Failed to import view into Gravitino: {}.{}.{}.{} - {}",
+ metalake,
+ catalogName,
+ namespace,
+ viewName,
+ e.getMessage());
+ }
+ }
+ }
+}
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
index 92a5e9f9bc..6415a8f066 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergOwnershipUtils.java
@@ -25,7 +25,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import org.apache.gravitino.Entity;
+import org.apache.gravitino.Entity.EntityType;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.authorization.Owner;
@@ -46,6 +46,7 @@ public class TestIcebergOwnershipUtils {
private static final String USER = "test_user";
private static final String SCHEMA_NAME = "test_schema";
private static final String TABLE_NAME = "test_table";
+ private static final String VIEW_NAME = "test_view";
private OwnerDispatcher mockOwnerDispatcher;
@@ -60,7 +61,7 @@ public class TestIcebergOwnershipUtils {
NameIdentifier expectedSchemaIdentifier =
IcebergIdentifierUtils.toGravitinoSchemaIdentifier(METALAKE, CATALOG,
namespace);
MetadataObject expectedMetadataObject =
- NameIdentifierUtil.toMetadataObject(expectedSchemaIdentifier,
Entity.EntityType.SCHEMA);
+ NameIdentifierUtil.toMetadataObject(expectedSchemaIdentifier,
EntityType.SCHEMA);
IcebergOwnershipUtils.setSchemaOwner(METALAKE, CATALOG, namespace, USER,
mockOwnerDispatcher);
@@ -75,7 +76,7 @@ public class TestIcebergOwnershipUtils {
NameIdentifier expectedTableIdentifier =
IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG,
tableIdentifier);
MetadataObject expectedMetadataObject =
- NameIdentifierUtil.toMetadataObject(expectedTableIdentifier,
Entity.EntityType.TABLE);
+ NameIdentifierUtil.toMetadataObject(expectedTableIdentifier,
EntityType.TABLE);
IcebergOwnershipUtils.setTableOwner(
METALAKE, CATALOG, namespace, TABLE_NAME, USER, mockOwnerDispatcher);
@@ -105,4 +106,31 @@ public class TestIcebergOwnershipUtils {
fail("setTableOwner should handle null dispatcher gracefully, but threw:
" + e.getMessage());
}
}
+
+ @Test
+ public void testSetViewOwner() {
+ Namespace namespace = Namespace.of(SCHEMA_NAME);
+ TableIdentifier viewIdentifier = TableIdentifier.of(namespace, VIEW_NAME);
+ NameIdentifier expectedViewIdentifier =
+ IcebergIdentifierUtils.toGravitinoTableIdentifier(METALAKE, CATALOG,
viewIdentifier);
+ MetadataObject expectedMetadataObject =
+ NameIdentifierUtil.toMetadataObject(expectedViewIdentifier,
EntityType.VIEW);
+
+ IcebergOwnershipUtils.setViewOwner(
+ METALAKE, CATALOG, namespace, VIEW_NAME, USER, mockOwnerDispatcher);
+
+ verify(mockOwnerDispatcher, times(1))
+ .setOwner(eq(METALAKE), eq(expectedMetadataObject), eq(USER),
eq(Owner.Type.USER));
+ }
+
+ @Test
+ public void testSetViewOwnerWithNullOwnerDispatcher() {
+ Namespace namespace = Namespace.of(SCHEMA_NAME);
+
+ try {
+ IcebergOwnershipUtils.setViewOwner(METALAKE, CATALOG, namespace,
VIEW_NAME, USER, null);
+ } catch (Exception e) {
+ fail("setViewOwner should handle null dispatcher gracefully, but threw:
" + e.getMessage());
+ }
+ }
}
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
new file mode 100644
index 0000000000..c0491677bd
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/dispatcher/TestIcebergViewOperationExecutor.java
@@ -0,0 +1,176 @@
+/*
+ * 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.gravitino.iceberg.service.dispatcher;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.gravitino.iceberg.service.CatalogWrapperForREST;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import org.apache.gravitino.listener.api.event.IcebergRequestContext;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.requests.CreateViewRequest;
+import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadViewResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestIcebergViewOperationExecutor {
+
+ private IcebergViewOperationExecutor executor;
+ private IcebergCatalogWrapperManager mockWrapperManager;
+ private CatalogWrapperForREST mockCatalogWrapper;
+ private IcebergRequestContext mockContext;
+
+ @BeforeEach
+ public void setUp() {
+ mockWrapperManager = mock(IcebergCatalogWrapperManager.class);
+ mockCatalogWrapper = mock(CatalogWrapperForREST.class);
+ executor = new IcebergViewOperationExecutor(mockWrapperManager);
+
+ mockContext = mock(IcebergRequestContext.class);
+ when(mockContext.catalogName()).thenReturn("test_catalog");
+
when(mockWrapperManager.getCatalogWrapper("test_catalog")).thenReturn(mockCatalogWrapper);
+ }
+
+ @Test
+ public void testCreateView() {
+ Namespace namespace = Namespace.of("test_ns");
+ CreateViewRequest mockRequest = mock(CreateViewRequest.class);
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockCatalogWrapper.createView(namespace,
mockRequest)).thenReturn(mockResponse);
+
+ LoadViewResponse result = executor.createView(mockContext, namespace,
mockRequest);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockCatalogWrapper).createView(namespace, mockRequest);
+ }
+
+ @Test
+ public void testLoadView() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockCatalogWrapper.loadView(viewId)).thenReturn(mockResponse);
+
+ LoadViewResponse result = executor.loadView(mockContext, viewId);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockCatalogWrapper).loadView(viewId);
+ }
+
+ @Test
+ public void testDropView() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+
+ executor.dropView(mockContext, viewId);
+
+ verify(mockCatalogWrapper).dropView(viewId);
+ }
+
+ @Test
+ public void testListView() {
+ Namespace namespace = Namespace.of("test_ns");
+ ListTablesResponse mockResponse = mock(ListTablesResponse.class);
+ when(mockCatalogWrapper.listView(namespace)).thenReturn(mockResponse);
+
+ ListTablesResponse result = executor.listView(mockContext, namespace);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockCatalogWrapper).listView(namespace);
+ }
+
+ @Test
+ public void testViewExists() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+ when(mockCatalogWrapper.viewExists(viewId)).thenReturn(true);
+
+ boolean result = executor.viewExists(mockContext, viewId);
+
+ Assertions.assertTrue(result);
+ verify(mockCatalogWrapper).viewExists(viewId);
+ }
+
+ @Test
+ public void testViewDoesNotExist() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns",
"non_existent_view");
+ when(mockCatalogWrapper.viewExists(viewId)).thenReturn(false);
+
+ boolean result = executor.viewExists(mockContext, viewId);
+
+ Assertions.assertFalse(result);
+ verify(mockCatalogWrapper).viewExists(viewId);
+ }
+
+ @Test
+ public void testLoadViewThrowsException() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+ RuntimeException exception = new RuntimeException("View not found");
+ when(mockCatalogWrapper.loadView(viewId)).thenThrow(exception);
+
+ RuntimeException thrown =
+ Assertions.assertThrows(
+ RuntimeException.class, () -> executor.loadView(mockContext,
viewId));
+
+ Assertions.assertEquals(exception, thrown);
+ verify(mockCatalogWrapper).loadView(viewId);
+ }
+
+ @Test
+ public void testCreateViewThrowsException() {
+ Namespace namespace = Namespace.of("test_ns");
+ CreateViewRequest mockRequest = mock(CreateViewRequest.class);
+ RuntimeException exception = new RuntimeException("Failed to create view");
+ when(mockCatalogWrapper.createView(namespace,
mockRequest)).thenThrow(exception);
+
+ RuntimeException thrown =
+ Assertions.assertThrows(
+ RuntimeException.class, () -> executor.createView(mockContext,
namespace, mockRequest));
+
+ Assertions.assertEquals(exception, thrown);
+ verify(mockCatalogWrapper).createView(namespace, mockRequest);
+ }
+
+ @Test
+ public void testReplaceView() {
+ TableIdentifier viewId = TableIdentifier.of("test_ns", "test_view");
+ UpdateTableRequest mockRequest = mock(UpdateTableRequest.class);
+ LoadViewResponse mockResponse = mock(LoadViewResponse.class);
+ when(mockCatalogWrapper.updateView(viewId,
mockRequest)).thenReturn(mockResponse);
+
+ LoadViewResponse result = executor.replaceView(mockContext, viewId,
mockRequest);
+
+ Assertions.assertEquals(mockResponse, result);
+ verify(mockCatalogWrapper).updateView(viewId, mockRequest);
+ }
+
+ @Test
+ public void testRenameView() {
+ RenameTableRequest mockRequest = mock(RenameTableRequest.class);
+
+ executor.renameView(mockContext, mockRequest);
+
+ verify(mockCatalogWrapper).renameView(mockRequest);
+ }
+}