Skip to content

Commit

Permalink
[#4370]feat(iceberg): support view interface for Iceberg REST server (#…
Browse files Browse the repository at this point in the history
…4937)

### What changes were proposed in this pull request?

support view interface for Iceberg REST server

### Why are the changes needed?

Fix: #4370

### Does this PR introduce _any_ user-facing change?

no

### How was this patch tested?

1. add UT
2. manual test

---------

Co-authored-by: theoryxu <[email protected]>
  • Loading branch information
theoryxu and theoryxu authored Oct 8, 2024
1 parent f600041 commit 13d1684
Show file tree
Hide file tree
Showing 11 changed files with 741 additions and 1 deletion.
10 changes: 9 additions & 1 deletion docs/iceberg-rest-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ The Apache Gravitino Iceberg REST Server follows the [Apache Iceberg REST API sp

- Supports the Apache Iceberg REST API defined in Iceberg 1.5, and supports all namespace and table interfaces. The following interfaces are not implemented yet:
- token
- view
- multi table transaction
- pagination
- Works as a catalog proxy, supporting `Hive` and `JDBC` as catalog backend.
Expand Down Expand Up @@ -214,6 +213,15 @@ You must download the corresponding JDBC driver to the `iceberg-rest-server/libs

If you want to use a custom Iceberg Catalog as `catalog-backend`, you can add a corresponding jar file to the classpath and load a custom Iceberg Catalog implementation by specifying the `catalog-backend-impl` property.

#### View support

You could access the view interface if using JDBC backend and enable `jdbc.schema-version` property.

| Configuration item | Description | Default value | Required | Since Version |
|-------------------------------------------------|--------------------------------------------------------------------------------------------|---------------|----------|---------------|
| `gravitino.iceberg-rest.jdbc.schema-version` | The schema version of JDBC catalog backend, setting to `V1` if supporting view operations. | (none) | NO | 0.7.0 |


#### Multi catalog support

The Gravitino Iceberg REST server supports multiple catalogs and offers a configuration-based catalog management system.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.SupportsNamespaces;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.catalog.ViewCatalog;
import org.apache.iceberg.rest.CatalogHandlers;
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.RegisterTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
Expand All @@ -55,6 +57,7 @@
import org.apache.iceberg.rest.responses.ListNamespacesResponse;
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;
Expand Down Expand Up @@ -116,6 +119,13 @@ private void validateNamespace(Optional<Namespace> namespace) {
}
}

private ViewCatalog getViewCatalog() {
if (!(catalog instanceof ViewCatalog)) {
throw new UnsupportedOperationException(catalog.name() + " is not support view");
}
return (ViewCatalog) catalog;
}

public CreateNamespaceResponse createNamespace(CreateNamespaceRequest request) {
validateNamespace(Optional.of(request.namespace()));
return CatalogHandlers.createNamespace(asNamespaceCatalog, request);
Expand Down Expand Up @@ -203,6 +213,37 @@ public LoadTableResponse updateTable(IcebergTableChange icebergTableChange) {
return loadTable(icebergTableChange.getTableIdentifier());
}

public LoadViewResponse createView(Namespace namespace, CreateViewRequest request) {
request.validate();
return CatalogHandlers.createView(getViewCatalog(), namespace, request);
}

public LoadViewResponse updateView(TableIdentifier viewIdentifier, UpdateTableRequest request) {
request.validate();
return CatalogHandlers.updateView(getViewCatalog(), viewIdentifier, request);
}

public LoadViewResponse loadView(TableIdentifier viewIdentifier) {
return CatalogHandlers.loadView(getViewCatalog(), viewIdentifier);
}

public void dropView(TableIdentifier viewIdentifier) {
CatalogHandlers.dropView(getViewCatalog(), viewIdentifier);
}

public void renameView(RenameTableRequest request) {
request.validate();
CatalogHandlers.renameView(getViewCatalog(), request);
}

public boolean existView(TableIdentifier viewIdentifier) {
return getViewCatalog().viewExists(viewIdentifier);
}

public ListTablesResponse listView(Namespace namespace) {
return CatalogHandlers.listViews(getViewCatalog(), namespace);
}

@Override
public void close() throws Exception {
if (catalog instanceof AutoCloseable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.iceberg.exceptions.NoSuchIcebergTableException;
import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.exceptions.NoSuchTableException;
import org.apache.iceberg.exceptions.NoSuchViewException;
import org.apache.iceberg.exceptions.NotAuthorizedException;
import org.apache.iceberg.exceptions.ServiceUnavailableException;
import org.apache.iceberg.exceptions.UnprocessableEntityException;
Expand All @@ -57,6 +58,7 @@ public class IcebergExceptionMapper implements ExceptionMapper<Exception> {
.put(NoSuchTableException.class, 404)
.put(NoSuchIcebergTableException.class, 404)
.put(UnsupportedOperationException.class, 406)
.put(NoSuchViewException.class, 404)
.put(AlreadyExistsException.class, 409)
.put(CommitFailedException.class, 409)
.put(UnprocessableEntityException.class, 422)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.rest;

import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
import org.apache.gravitino.iceberg.service.IcebergRestUtils;
import org.apache.gravitino.metrics.MetricNames;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ListTablesResponse;
import org.apache.iceberg.rest.responses.LoadViewResponse;

@Path("/v1/{prefix:([^/]*/)?}namespaces/{namespace}/views")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class IcebergViewOperations {

private IcebergCatalogWrapperManager icebergCatalogWrapperManager;

@SuppressWarnings("UnusedVariable")
@Context
private HttpServletRequest httpRequest;

@Inject
public IcebergViewOperations(IcebergCatalogWrapperManager icebergCatalogWrapperManager) {
this.icebergCatalogWrapperManager = icebergCatalogWrapperManager;
}

@GET
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "list-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "list-view", absolute = true)
public Response listView(
@PathParam("prefix") String prefix, @PathParam("namespace") String namespace) {
ListTablesResponse response =
icebergCatalogWrapperManager.getOps(prefix).listView(RESTUtil.decodeNamespace(namespace));
return IcebergRestUtils.ok(response);
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "create-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "create-view", absolute = true)
public Response createView(
@PathParam("prefix") String prefix,
@PathParam("namespace") String namespace,
CreateViewRequest request) {
LoadViewResponse response =
icebergCatalogWrapperManager
.getOps(prefix)
.createView(RESTUtil.decodeNamespace(namespace), request);
return IcebergRestUtils.ok(response);
}

@GET
@Path("{view}")
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "load-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "load-view", absolute = true)
public Response loadView(
@PathParam("prefix") String prefix,
@PathParam("namespace") String namespace,
@PathParam("view") String view) {
TableIdentifier viewIdentifier = TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
LoadViewResponse response =
icebergCatalogWrapperManager.getOps(prefix).loadView(viewIdentifier);
return IcebergRestUtils.ok(response);
}

@POST
@Path("{view}")
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "replace-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "replace-view", absolute = true)
public Response replaceView(
@PathParam("prefix") String prefix,
@PathParam("namespace") String namespace,
@PathParam("view") String view,
UpdateTableRequest request) {
TableIdentifier viewIdentifier = TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
LoadViewResponse response =
icebergCatalogWrapperManager.getOps(prefix).updateView(viewIdentifier, request);
return IcebergRestUtils.ok(response);
}

@DELETE
@Path("{view}")
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "drop-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "drop-view", absolute = true)
public Response dropView(
@PathParam("prefix") String prefix,
@PathParam("namespace") String namespace,
@PathParam("view") String view) {
TableIdentifier viewIdentifier = TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
icebergCatalogWrapperManager.getOps(prefix).dropView(viewIdentifier);
return IcebergRestUtils.noContent();
}

@HEAD
@Path("{view}")
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "view-exists." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "view-exits", absolute = true)
public Response viewExists(
@PathParam("prefix") String prefix,
@PathParam("namespace") String namespace,
@PathParam("view") String view) {
TableIdentifier tableIdentifier = TableIdentifier.of(RESTUtil.decodeNamespace(namespace), view);
if (icebergCatalogWrapperManager.getOps(prefix).existView(tableIdentifier)) {
return IcebergRestUtils.noContent();
} else {
return IcebergRestUtils.notExists();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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.rest;

import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
import org.apache.gravitino.iceberg.service.IcebergRestUtils;
import org.apache.gravitino.metrics.MetricNames;
import org.apache.iceberg.rest.requests.RenameTableRequest;

@Path("/v1/{prefix:([^/]*/)?}views/rename")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class IcebergViewRenameOperations {

@SuppressWarnings("UnusedVariable")
@Context
private HttpServletRequest httpRequest;

private IcebergCatalogWrapperManager icebergCatalogWrapperManager;

@Inject
public IcebergViewRenameOperations(IcebergCatalogWrapperManager icebergCatalogWrapperManager) {
this.icebergCatalogWrapperManager = icebergCatalogWrapperManager;
}

@POST
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "rename-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "rename-view", absolute = true)
public Response renameView(@PathParam("prefix") String prefix, RenameTableRequest request) {
icebergCatalogWrapperManager.getOps(prefix).renameView(request);
return IcebergRestUtils.noContent();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public Map<String, String> getCatalogConfig() {
configMap.put(
IcebergConfig.ICEBERG_CONFIG_PREFIX + IcebergConfig.JDBC_INIT_TABLES.getKey(), "true");

configMap.put(IcebergConfig.ICEBERG_CONFIG_PREFIX + "jdbc.schema-version", "V1");

configMap.put(
IcebergConfig.ICEBERG_CONFIG_PREFIX + IcebergConfig.CATALOG_WAREHOUSE.getKey(),
GravitinoITUtils.genRandomName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ boolean catalogTypeNotMemory() {
return !catalogType.equals(IcebergCatalogBackend.MEMORY);
}

boolean isSupportsViewCatalog() {
return !catalogType.equals(IcebergCatalogBackend.HIVE);
}

abstract void initEnv();

abstract Map<String, String> getCatalogConfig();
Expand Down Expand Up @@ -175,6 +179,10 @@ protected Map<String, String> getTableInfo(String tableName) {
return convertToStringMap(sql("desc table extended " + tableName));
}

protected Map<String, String> getViewInfo(String viewName) {
return convertToStringMap(sql("desc extended " + viewName));
}

protected List<String> getTableColumns(String tableName) {
List<Object[]> objects = sql("desc table extended " + tableName);
List<String> columns = new ArrayList<>();
Expand Down
Loading

0 comments on commit 13d1684

Please sign in to comment.