diff --git a/paimon-common/src/main/java/org/apache/paimon/types/RowType.java b/paimon-common/src/main/java/org/apache/paimon/types/RowType.java index 681a07af584d..d0c0881b6aa3 100644 --- a/paimon-common/src/main/java/org/apache/paimon/types/RowType.java +++ b/paimon-common/src/main/java/org/apache/paimon/types/RowType.java @@ -24,6 +24,8 @@ import org.apache.paimon.utils.Preconditions; import org.apache.paimon.utils.StringUtils; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonGenerator; import java.io.IOException; @@ -52,6 +54,8 @@ public final class RowType extends DataType { private static final long serialVersionUID = 1L; + private static final String FIELD_FIELDS = "fields"; + public static final String FORMAT = "ROW<%s>"; private final List fields; @@ -67,7 +71,8 @@ public RowType(boolean isNullable, List fields) { validateFields(fields); } - public RowType(List fields) { + @JsonCreator + public RowType(@JsonProperty(FIELD_FIELDS) List fields) { this(true, fields); } diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java index b2bbcbb7673a..87b8e8cb9612 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java @@ -45,6 +45,7 @@ import org.apache.paimon.rest.requests.CreateDatabaseRequest; import org.apache.paimon.rest.requests.CreatePartitionsRequest; import org.apache.paimon.rest.requests.CreateTableRequest; +import org.apache.paimon.rest.requests.CreateViewRequest; import org.apache.paimon.rest.requests.DropPartitionsRequest; import org.apache.paimon.rest.requests.MarkDonePartitionsRequest; import org.apache.paimon.rest.requests.RenameTableRequest; @@ -55,16 +56,22 @@ import org.apache.paimon.rest.responses.ErrorResponseResourceType; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetTableResponse; +import org.apache.paimon.rest.responses.GetViewResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.rest.responses.ListPartitionsResponse; import org.apache.paimon.rest.responses.ListTablesResponse; +import org.apache.paimon.rest.responses.ListViewsResponse; import org.apache.paimon.schema.Schema; import org.apache.paimon.schema.SchemaChange; import org.apache.paimon.schema.TableSchema; import org.apache.paimon.table.FileStoreTable; import org.apache.paimon.table.Table; import org.apache.paimon.table.sink.BatchWriteBuilder; +import org.apache.paimon.types.RowType; import org.apache.paimon.utils.Pair; +import org.apache.paimon.view.View; +import org.apache.paimon.view.ViewImpl; +import org.apache.paimon.view.ViewSchema; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList; @@ -290,8 +297,7 @@ public boolean commitSnapshot(Identifier identifier, Snapshot snapshot) { CommitTableRequest request = new CommitTableRequest(identifier, snapshot); CommitTableResponse response = client.post( - resourcePaths.commitTable( - identifier.getDatabaseName(), identifier.getTableName()), + resourcePaths.commitTable(identifier.getDatabaseName()), request, CommitTableResponse.class, headers()); @@ -325,11 +331,7 @@ public void createTable(Identifier identifier, Schema schema, boolean ignoreIfEx checkNotSystemTable(identifier, "createTable"); validateAutoCreateClose(schema.options()); CreateTableRequest request = new CreateTableRequest(identifier, schema); - client.post( - resourcePaths.tables(identifier.getDatabaseName()), - request, - GetTableResponse.class, - headers()); + client.post(resourcePaths.tables(identifier.getDatabaseName()), request, headers()); } catch (AlreadyExistsException e) { if (!ignoreIfExists) { throw new TableAlreadyExistException(identifier); @@ -353,13 +355,8 @@ public void renameTable(Identifier fromTable, Identifier toTable, boolean ignore checkNotSystemTable(fromTable, "renameTable"); checkNotSystemTable(toTable, "renameTable"); try { - RenameTableRequest request = new RenameTableRequest(toTable); - client.post( - resourcePaths.renameTable( - fromTable.getDatabaseName(), fromTable.getTableName()), - request, - GetTableResponse.class, - headers()); + RenameTableRequest request = new RenameTableRequest(fromTable, toTable); + client.post(resourcePaths.renameTable(fromTable.getDatabaseName()), request, headers()); } catch (NoSuchResourceException e) { if (!ignoreIfNotExists) { throw new TableNotExistException(fromTable); @@ -381,7 +378,6 @@ public void alterTable( client.post( resourcePaths.table(identifier.getDatabaseName(), identifier.getTableName()), request, - GetTableResponse.class, headers()); } catch (NoSuchResourceException e) { if (!ignoreIfNotExists) { @@ -532,6 +528,88 @@ public List listPartitions(Identifier identifier) throws TableNotExis return response.getPartitions(); } + @Override + public View getView(Identifier identifier) throws ViewNotExistException { + try { + GetViewResponse response = + client.get( + resourcePaths.view( + identifier.getDatabaseName(), identifier.getTableName()), + GetViewResponse.class, + headers()); + return new ViewImpl( + identifier, + response.getSchema().rowType(), + response.getSchema().query(), + response.getSchema().comment(), + response.getSchema().options()); + } catch (NoSuchResourceException e) { + throw new ViewNotExistException(identifier); + } + } + + @Override + public void dropView(Identifier identifier, boolean ignoreIfNotExists) + throws ViewNotExistException { + try { + client.delete( + resourcePaths.view(identifier.getDatabaseName(), identifier.getTableName()), + headers()); + } catch (NoSuchResourceException e) { + if (!ignoreIfNotExists) { + throw new ViewNotExistException(identifier); + } + } + } + + @Override + public void createView(Identifier identifier, View view, boolean ignoreIfExists) + throws ViewAlreadyExistException, DatabaseNotExistException { + try { + ViewSchema schema = + new ViewSchema( + new RowType(view.rowType().getFields()), + view.options(), + view.comment().orElse(null), + view.query()); + CreateViewRequest request = new CreateViewRequest(identifier, schema); + client.post(resourcePaths.views(identifier.getDatabaseName()), request, headers()); + } catch (NoSuchResourceException e) { + throw new DatabaseNotExistException(identifier.getDatabaseName()); + } catch (AlreadyExistsException e) { + if (!ignoreIfExists) { + throw new ViewAlreadyExistException(identifier); + } + } + } + + @Override + public List listViews(String databaseName) throws DatabaseNotExistException { + try { + ListViewsResponse response = + client.get( + resourcePaths.views(databaseName), ListViewsResponse.class, headers()); + return response.getViews(); + } catch (NoSuchResourceException e) { + throw new DatabaseNotExistException(databaseName); + } + } + + @Override + public void renameView(Identifier fromView, Identifier toView, boolean ignoreIfNotExists) + throws ViewNotExistException, ViewAlreadyExistException { + try { + RenameTableRequest request = new RenameTableRequest(fromView, toView); + client.post(resourcePaths.renameView(fromView.getDatabaseName()), request, headers()); + } catch (NoSuchResourceException e) { + if (!ignoreIfNotExists) { + throw new ViewNotExistException(fromView); + } + } catch (AlreadyExistsException e) { + throw new ViewAlreadyExistException(toView); + } + } + @Override public boolean caseSensitive() { return options.getOptional(CASE_SENSITIVE).orElse(true); diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java index a41dad25df56..d77475fe40dc 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java @@ -62,12 +62,12 @@ public String table(String databaseName, String tableName) { return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, tableName); } - public String renameTable(String databaseName, String tableName) { - return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, tableName, "rename"); + public String renameTable(String databaseName) { + return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, "rename"); } - public String commitTable(String databaseName, String tableName) { - return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, tableName, "commit"); + public String commitTable(String databaseName) { + return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, "commit"); } public String partitions(String databaseName, String tableName) { @@ -88,4 +88,16 @@ public String markDonePartitions(String databaseName, String tableName) { return SLASH.join( V1, prefix, DATABASES, databaseName, TABLES, tableName, "partitions", "mark"); } + + public String views(String databaseName) { + return SLASH.join(V1, prefix, DATABASES, databaseName, "views"); + } + + public String view(String databaseName, String viewName) { + return SLASH.join(V1, prefix, DATABASES, databaseName, "views", viewName); + } + + public String renameView(String databaseName) { + return SLASH.join(V1, prefix, DATABASES, databaseName, "views", "rename"); + } } diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateViewRequest.java b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateViewRequest.java new file mode 100644 index 000000000000..0951f822a96d --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateViewRequest.java @@ -0,0 +1,60 @@ +/* + * 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.paimon.rest.requests; + +import org.apache.paimon.catalog.Identifier; +import org.apache.paimon.rest.RESTRequest; +import org.apache.paimon.view.ViewSchema; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +/** Request for creating view. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateViewRequest implements RESTRequest { + + private static final String FIELD_IDENTIFIER = "identifier"; + private static final String FIELD_SCHEMA = "schema"; + + @JsonProperty(FIELD_IDENTIFIER) + private final Identifier identifier; + + @JsonProperty(FIELD_SCHEMA) + private final ViewSchema schema; + + @JsonCreator + public CreateViewRequest( + @JsonProperty(FIELD_IDENTIFIER) Identifier identifier, + @JsonProperty(FIELD_SCHEMA) ViewSchema schema) { + this.schema = schema; + this.identifier = identifier; + } + + @JsonGetter(FIELD_IDENTIFIER) + public Identifier getIdentifier() { + return identifier; + } + + @JsonGetter(FIELD_SCHEMA) + public ViewSchema getSchema() { + return schema; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/requests/RenameTableRequest.java b/paimon-core/src/main/java/org/apache/paimon/rest/requests/RenameTableRequest.java index fd2eb4f9518b..483e26538e5e 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/requests/RenameTableRequest.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/requests/RenameTableRequest.java @@ -26,22 +26,34 @@ import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; -/** Request for renaming table. */ +/** Request for renaming. */ @JsonIgnoreProperties(ignoreUnknown = true) public class RenameTableRequest implements RESTRequest { - private static final String FIELD_NEW_IDENTIFIER_NAME = "newIdentifier"; + private static final String FIELD_SOURCE = "source"; + private static final String FIELD_DESTINATION = "destination"; - @JsonProperty(FIELD_NEW_IDENTIFIER_NAME) - private final Identifier newIdentifier; + @JsonProperty(FIELD_SOURCE) + private final Identifier source; + + @JsonProperty(FIELD_DESTINATION) + private final Identifier destination; @JsonCreator - public RenameTableRequest(@JsonProperty(FIELD_NEW_IDENTIFIER_NAME) Identifier newIdentifier) { - this.newIdentifier = newIdentifier; + public RenameTableRequest( + @JsonProperty(FIELD_SOURCE) Identifier source, + @JsonProperty(FIELD_DESTINATION) Identifier destination) { + this.source = source; + this.destination = destination; + } + + @JsonGetter(FIELD_DESTINATION) + public Identifier getDestination() { + return destination; } - @JsonGetter(FIELD_NEW_IDENTIFIER_NAME) - public Identifier getNewIdentifier() { - return newIdentifier; + @JsonGetter(FIELD_SOURCE) + public Identifier getSource() { + return source; } } diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponseResourceType.java b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponseResourceType.java index 590f38e720d4..5dc6cffade1a 100644 --- a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponseResourceType.java +++ b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponseResourceType.java @@ -23,4 +23,5 @@ public enum ErrorResponseResourceType { DATABASE, TABLE, COLUMN, + VIEW } diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetViewResponse.java b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetViewResponse.java new file mode 100644 index 000000000000..7fe1237691b1 --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetViewResponse.java @@ -0,0 +1,70 @@ +/* + * 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.paimon.rest.responses; + +import org.apache.paimon.rest.RESTResponse; +import org.apache.paimon.view.ViewSchema; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +/** Response for getting view. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetViewResponse implements RESTResponse { + + private static final String FIELD_ID = "id"; + private static final String FIELD_NAME = "name"; + private static final String FIELD_SCHEMA = "schema"; + + @JsonProperty(FIELD_ID) + private final String id; + + @JsonProperty(FIELD_NAME) + private final String name; + + @JsonProperty(FIELD_SCHEMA) + private final ViewSchema schema; + + @JsonCreator + public GetViewResponse( + @JsonProperty(FIELD_ID) String id, + @JsonProperty(FIELD_NAME) String name, + @JsonProperty(FIELD_SCHEMA) ViewSchema schema) { + this.id = id; + this.name = name; + this.schema = schema; + } + + @JsonGetter(FIELD_ID) + public String getId() { + return this.id; + } + + @JsonGetter(FIELD_NAME) + public String getName() { + return this.name; + } + + @JsonGetter(FIELD_SCHEMA) + public ViewSchema getSchema() { + return this.schema; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListViewsResponse.java b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListViewsResponse.java new file mode 100644 index 000000000000..d785fd68c2b8 --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListViewsResponse.java @@ -0,0 +1,48 @@ +/* + * 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.paimon.rest.responses; + +import org.apache.paimon.rest.RESTResponse; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** Response for listing tables. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ListViewsResponse implements RESTResponse { + + private static final String FIELD_VIEWS = "views"; + + @JsonProperty(FIELD_VIEWS) + private final List views; + + @JsonCreator + public ListViewsResponse(@JsonProperty(FIELD_VIEWS) List views) { + this.views = views; + } + + @JsonGetter(FIELD_VIEWS) + public List getViews() { + return this.views; + } +} diff --git a/paimon-core/src/main/java/org/apache/paimon/view/ViewImpl.java b/paimon-core/src/main/java/org/apache/paimon/view/ViewImpl.java index 1cd48d4ce445..8edb517d93ec 100644 --- a/paimon-core/src/main/java/org/apache/paimon/view/ViewImpl.java +++ b/paimon-core/src/main/java/org/apache/paimon/view/ViewImpl.java @@ -21,6 +21,8 @@ import org.apache.paimon.catalog.Identifier; import org.apache.paimon.types.RowType; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import javax.annotation.Nullable; import java.util.HashMap; @@ -29,13 +31,11 @@ import java.util.Optional; /** Implementation of {@link View}. */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ViewImpl implements View { private final Identifier identifier; - private final RowType rowType; - private final String query; - @Nullable private final String comment; - private final Map options; + private final ViewSchema viewSchema; public ViewImpl( Identifier identifier, @@ -44,10 +44,7 @@ public ViewImpl( @Nullable String comment, Map options) { this.identifier = identifier; - this.rowType = rowType; - this.query = query; - this.comment = comment; - this.options = options; + this.viewSchema = new ViewSchema(query, comment, options, rowType); } @Override @@ -62,29 +59,29 @@ public String fullName() { @Override public RowType rowType() { - return rowType; + return this.viewSchema.rowType(); } @Override public String query() { - return query; + return this.viewSchema.query(); } @Override public Optional comment() { - return Optional.ofNullable(comment); + return Optional.ofNullable(this.viewSchema.comment()); } @Override public Map options() { - return options; + return this.viewSchema.options(); } @Override public View copy(Map dynamicOptions) { - Map newOptions = new HashMap<>(options); + Map newOptions = new HashMap<>(options()); newOptions.putAll(dynamicOptions); - return new ViewImpl(identifier, rowType, query, comment, newOptions); + return new ViewImpl(identifier, rowType(), query(), this.viewSchema.comment(), newOptions); } @Override @@ -97,14 +94,11 @@ public boolean equals(Object o) { } ViewImpl view = (ViewImpl) o; return Objects.equals(identifier, view.identifier) - && Objects.equals(rowType, view.rowType) - && Objects.equals(query, view.query) - && Objects.equals(comment, view.comment) - && Objects.equals(options, view.options); + && Objects.equals(viewSchema, view.viewSchema); } @Override public int hashCode() { - return Objects.hash(identifier, rowType, query, comment, options); + return Objects.hash(identifier, viewSchema); } } diff --git a/paimon-core/src/main/java/org/apache/paimon/view/ViewSchema.java b/paimon-core/src/main/java/org/apache/paimon/view/ViewSchema.java new file mode 100644 index 000000000000..ff64f9d5c068 --- /dev/null +++ b/paimon-core/src/main/java/org/apache/paimon/view/ViewSchema.java @@ -0,0 +1,113 @@ +/* + * 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.paimon.view; + +import org.apache.paimon.types.RowType; + +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonInclude; +import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.Nullable; + +import java.util.Map; +import java.util.Objects; + +/** Schema for view. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ViewSchema { + private static final String FIELD_FIELDS = "rowType"; + private static final String FIELD_OPTIONS = "options"; + private static final String FIELD_COMMENT = "comment"; + private static final String FIELD_QUERY = "query"; + + @JsonProperty(FIELD_QUERY) + private final String query; + + @Nullable + @JsonProperty(FIELD_COMMENT) + @JsonInclude(JsonInclude.Include.NON_NULL) + private final String comment; + + @JsonProperty(FIELD_OPTIONS) + private final Map options; + + @JsonProperty(FIELD_FIELDS) + private final RowType rowType; + + @JsonCreator + public ViewSchema( + @JsonProperty(FIELD_FIELDS) RowType rowType, + @JsonProperty(FIELD_OPTIONS) Map options, + @Nullable @JsonProperty(FIELD_COMMENT) String comment, + @JsonProperty(FIELD_QUERY) String query) { + this.options = options; + this.comment = comment; + this.query = query; + this.rowType = rowType; + } + + public ViewSchema( + String query, @Nullable String comment, Map options, RowType rowType) { + this.query = query; + this.comment = comment; + this.options = options; + this.rowType = rowType; + } + + @JsonGetter(FIELD_FIELDS) + public RowType rowType() { + return rowType; + } + + @JsonGetter(FIELD_QUERY) + public String query() { + return query; + } + + @Nullable + @JsonGetter(FIELD_COMMENT) + public String comment() { + return comment; + } + + @JsonGetter(FIELD_OPTIONS) + public Map options() { + return options; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + ViewSchema that = (ViewSchema) o; + return Objects.equals(query, that.query) + && Objects.equals(comment, that.comment) + && Objects.equals(options, that.options) + && Objects.equals(rowType, that.rowType); + } + + @Override + public int hashCode() { + return Objects.hash(query, comment, options, rowType); + } +} diff --git a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java index 1c453a1b3bcf..b8da733e7740 100644 --- a/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java +++ b/paimon-core/src/test/java/org/apache/paimon/catalog/CatalogTestBase.java @@ -970,7 +970,6 @@ public void testView() throws Exception { .isInstanceOf(Catalog.ViewNotExistException.class); catalog.renameView(identifier, newIdentifier, false); - catalog.dropView(newIdentifier, false); catalog.dropView(newIdentifier, true); assertThatThrownBy(() -> catalog.dropView(newIdentifier, false)) .isInstanceOf(Catalog.ViewNotExistException.class); diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java index 822e06d7fbbc..766cb09b0bdd 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java @@ -26,15 +26,18 @@ import org.apache.paimon.rest.requests.CreateDatabaseRequest; import org.apache.paimon.rest.requests.CreatePartitionsRequest; import org.apache.paimon.rest.requests.CreateTableRequest; +import org.apache.paimon.rest.requests.CreateViewRequest; import org.apache.paimon.rest.requests.DropPartitionsRequest; import org.apache.paimon.rest.requests.RenameTableRequest; import org.apache.paimon.rest.responses.AlterDatabaseResponse; import org.apache.paimon.rest.responses.CreateDatabaseResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetTableResponse; +import org.apache.paimon.rest.responses.GetViewResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.rest.responses.ListPartitionsResponse; import org.apache.paimon.rest.responses.ListTablesResponse; +import org.apache.paimon.rest.responses.ListViewsResponse; import org.apache.paimon.schema.Schema; import org.apache.paimon.schema.SchemaChange; import org.apache.paimon.types.DataField; @@ -42,6 +45,7 @@ import org.apache.paimon.types.DataTypes; import org.apache.paimon.types.IntType; import org.apache.paimon.types.RowType; +import org.apache.paimon.view.ViewSchema; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList; import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; @@ -124,9 +128,10 @@ public static CreateTableRequest createTableRequest(String name) { return new CreateTableRequest(identifier, schema); } - public static RenameTableRequest renameRequest(String toTableName) { - Identifier newIdentifier = Identifier.create(databaseName(), toTableName); - return new RenameTableRequest(newIdentifier); + public static RenameTableRequest renameRequest(String sourceTable, String toTableName) { + Identifier source = Identifier.create(databaseName(), sourceTable); + Identifier destination = Identifier.create(databaseName(), toTableName); + return new RenameTableRequest(source, destination); } public static AlterTableRequest alterTableRequest() { @@ -230,6 +235,31 @@ public static AlterPartitionsRequest alterPartitionsRequest() { return new AlterPartitionsRequest(ImmutableList.of(partition())); } + public static CreateViewRequest createViewRequest(String name) { + Identifier identifier = Identifier.create(databaseName(), name); + return new CreateViewRequest(identifier, viewSchema()); + } + + public static GetViewResponse getViewResponse() { + return new GetViewResponse(UUID.randomUUID().toString(), "", viewSchema()); + } + + public static ListViewsResponse listViewsResponse() { + return new ListViewsResponse(ImmutableList.of("view")); + } + + private static ViewSchema viewSchema() { + List fields = + Arrays.asList( + new DataField(0, "f0", new IntType()), + new DataField(1, "f1", new IntType())); + return new ViewSchema( + new RowType(fields), + Collections.singletonMap("pt", "1"), + "comment", + "select * from t1"); + } + private static Partition partition() { return new Partition(Collections.singletonMap("pt", "1"), 1, 1, 1, 1); } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java index 21284629ccc9..91024867b7ea 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java @@ -33,6 +33,7 @@ import org.apache.paimon.rest.requests.CreateDatabaseRequest; import org.apache.paimon.rest.requests.CreatePartitionsRequest; import org.apache.paimon.rest.requests.CreateTableRequest; +import org.apache.paimon.rest.requests.CreateViewRequest; import org.apache.paimon.rest.requests.DropPartitionsRequest; import org.apache.paimon.rest.requests.MarkDonePartitionsRequest; import org.apache.paimon.rest.requests.RenameTableRequest; @@ -42,14 +43,19 @@ import org.apache.paimon.rest.responses.ErrorResponseResourceType; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetTableResponse; +import org.apache.paimon.rest.responses.GetViewResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.rest.responses.ListPartitionsResponse; import org.apache.paimon.rest.responses.ListTablesResponse; +import org.apache.paimon.rest.responses.ListViewsResponse; import org.apache.paimon.schema.Schema; import org.apache.paimon.table.FileStoreTable; import org.apache.paimon.table.FormatTable; import org.apache.paimon.table.Table; import org.apache.paimon.types.DataField; +import org.apache.paimon.view.View; +import org.apache.paimon.view.ViewImpl; +import org.apache.paimon.view.ViewSchema; import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException; @@ -120,12 +126,29 @@ public MockResponse dispatch(RecordedRequest request) { .substring((DATABASE_URI + "/").length()) .split("/"); String databaseName = resources[0]; + boolean isViews = resources.length == 2 && "views".equals(resources[1]); boolean isTables = resources.length == 2 && "tables".equals(resources[1]); - boolean isTable = resources.length == 3 && "tables".equals(resources[1]); boolean isTableRename = - resources.length == 4 && "rename".equals(resources[3]); + resources.length == 3 + && "tables".equals(resources[1]) + && "rename".equals(resources[2]); + boolean isViewRename = + resources.length == 3 + && "views".equals(resources[1]) + && "rename".equals(resources[2]); + boolean isView = + resources.length == 3 + && "views".equals(resources[1]) + && !"rename".equals(resources[2]); + boolean isTable = + resources.length == 3 + && "tables".equals(resources[1]) + && !"rename".equals(resources[2]) + && !"commit".equals(resources[2]); boolean isTableCommit = - resources.length == 4 && "commit".equals(resources[3]); + resources.length == 3 + && "tables".equals(resources[1]) + && "commit".equals(resources[2]); boolean isPartitions = resources.length == 4 && "tables".equals(resources[1]) @@ -180,8 +203,7 @@ public MockResponse dispatch(RecordedRequest request) { String tableName = resources[2]; return partitionsApiHandler(catalog, request, databaseName, tableName); } else if (isTableRename) { - return renameTableApiHandler( - catalog, request, databaseName, resources[2]); + return renameTableApiHandler(catalog, request); } else if (isTableCommit) { return commitTableApiHandler( catalog, request, databaseName, resources[2]); @@ -190,6 +212,13 @@ public MockResponse dispatch(RecordedRequest request) { return tableApiHandler(catalog, request, databaseName, tableName); } else if (isTables) { return tablesApiHandler(catalog, request, databaseName); + } else if (isViews) { + return viewsApiHandler(catalog, request, databaseName); + } else if (isViewRename) { + return renameViewApiHandler(catalog, request); + } else if (isView) { + String viewName = resources[2]; + return viewApiHandler(catalog, request, databaseName, viewName); } else { return databaseApiHandler(catalog, request, databaseName); } @@ -243,6 +272,22 @@ public MockResponse dispatch(RecordedRequest request) { e.getMessage(), 409); return mockResponse(response, 409); + } catch (Catalog.ViewNotExistException e) { + response = + new ErrorResponse( + ErrorResponseResourceType.VIEW, + e.identifier().getTableName(), + e.getMessage(), + 404); + return mockResponse(response, 404); + } catch (Catalog.ViewAlreadyExistException e) { + response = + new ErrorResponse( + ErrorResponseResourceType.VIEW, + e.identifier().getTableName(), + e.getMessage(), + 409); + return mockResponse(response, 409); } catch (IllegalArgumentException e) { response = new ErrorResponse(null, null, e.getMessage(), 400); return mockResponse(response, 400); @@ -265,21 +310,6 @@ public MockResponse dispatch(RecordedRequest request) { }; } - private static MockResponse renameTableApiHandler( - Catalog catalog, RecordedRequest request, String databaseName, String tableName) - throws Exception { - RenameTableRequest requestBody = - OBJECT_MAPPER.readValue(request.getBody().readUtf8(), RenameTableRequest.class); - catalog.renameTable( - Identifier.create(databaseName, tableName), requestBody.getNewIdentifier(), false); - GetTableResponse response = - getTable( - catalog, - requestBody.getNewIdentifier().getDatabaseName(), - requestBody.getNewIdentifier().getTableName()); - return mockResponse(response, 200); - } - private static MockResponse commitTableApiHandler( Catalog catalog, RecordedRequest request, String databaseName, String tableName) throws Exception { @@ -342,7 +372,6 @@ private static MockResponse tablesApiHandler( RESTResponse response; switch (request.getMethod()) { case "GET": - catalog.listTables(databaseName); response = new ListTablesResponse(catalog.listTables(databaseName)); return mockResponse(response, 200); case "POST": @@ -350,10 +379,7 @@ private static MockResponse tablesApiHandler( OBJECT_MAPPER.readValue( request.getBody().readUtf8(), CreateTableRequest.class); catalog.createTable(requestBody.getIdentifier(), requestBody.getSchema(), false); - response = - new GetTableResponse( - UUID.randomUUID().toString(), "", 1L, requestBody.getSchema()); - return mockResponse(response, 200); + return new MockResponse().setResponseCode(200); default: return new MockResponse().setResponseCode(404); } @@ -373,8 +399,7 @@ private static MockResponse tableApiHandler( OBJECT_MAPPER.readValue( request.getBody().readUtf8(), AlterTableRequest.class); catalog.alterTable(identifier, requestBody.getChanges(), false); - response = getTable(catalog, databaseName, tableName); - return mockResponse(response, 200); + return new MockResponse().setResponseCode(200); case "DELETE": catalog.dropTable(identifier, false); return new MockResponse().setResponseCode(200); @@ -383,6 +408,14 @@ private static MockResponse tableApiHandler( } } + private static MockResponse renameTableApiHandler(Catalog catalog, RecordedRequest request) + throws Exception { + RenameTableRequest requestBody = + OBJECT_MAPPER.readValue(request.getBody().readUtf8(), RenameTableRequest.class); + catalog.renameTable(requestBody.getSource(), requestBody.getDestination(), false); + return new MockResponse().setResponseCode(200); + } + private static MockResponse partitionsApiHandler( Catalog catalog, RecordedRequest request, String databaseName, String tableName) throws Exception { @@ -404,6 +437,63 @@ private static MockResponse partitionsApiHandler( } } + private static MockResponse viewsApiHandler( + Catalog catalog, RecordedRequest request, String databaseName) throws Exception { + RESTResponse response; + switch (request.getMethod()) { + case "GET": + response = new ListViewsResponse(catalog.listViews(databaseName)); + return mockResponse(response, 200); + case "POST": + CreateViewRequest requestBody = + OBJECT_MAPPER.readValue( + request.getBody().readUtf8(), CreateViewRequest.class); + ViewImpl view = + new ViewImpl( + requestBody.getIdentifier(), + requestBody.getSchema().rowType(), + requestBody.getSchema().query(), + requestBody.getSchema().comment(), + requestBody.getSchema().options()); + catalog.createView(requestBody.getIdentifier(), view, false); + return new MockResponse().setResponseCode(200); + default: + return new MockResponse().setResponseCode(404); + } + } + + private static MockResponse viewApiHandler( + Catalog catalog, RecordedRequest request, String databaseName, String viewName) + throws Exception { + RESTResponse response; + Identifier identifier = Identifier.create(databaseName, viewName); + switch (request.getMethod()) { + case "GET": + View view = catalog.getView(identifier); + ViewSchema schema = + new ViewSchema( + view.rowType(), + view.options(), + view.comment().orElse(null), + view.query()); + response = new GetViewResponse("id", identifier.getTableName(), schema); + return mockResponse(response, 200); + case "DELETE": + catalog.dropView(identifier, false); + return new MockResponse().setResponseCode(200); + default: + return new MockResponse().setResponseCode(404); + } + } + + private static MockResponse renameViewApiHandler(Catalog catalog, RecordedRequest request) + throws Exception { + RenameTableRequest requestBody = + OBJECT_MAPPER.readValue(request.getBody().readUtf8(), RenameTableRequest.class); + catalog.renameView(requestBody.getSource(), requestBody.getDestination(), false); + return new MockResponse().setResponseCode(200); + } + private static GetTableResponse getTable(Catalog catalog, String databaseName, String tableName) throws Exception { Identifier identifier = Identifier.create(databaseName, tableName); diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java index f63d70332650..d1ce64b6c543 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java @@ -117,6 +117,11 @@ protected boolean supportPartitions() { return true; } + @Override + protected boolean supportsView() { + return true; + } + private void createTable( Identifier identifier, Map options, List partitionKeys) throws Exception { diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java index 6e260a5e7341..fa56f8111828 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java @@ -24,6 +24,7 @@ import org.apache.paimon.rest.requests.CreateDatabaseRequest; import org.apache.paimon.rest.requests.CreatePartitionsRequest; import org.apache.paimon.rest.requests.CreateTableRequest; +import org.apache.paimon.rest.requests.CreateViewRequest; import org.apache.paimon.rest.requests.DropPartitionsRequest; import org.apache.paimon.rest.requests.RenameTableRequest; import org.apache.paimon.rest.responses.AlterDatabaseResponse; @@ -32,9 +33,11 @@ import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetTableResponse; +import org.apache.paimon.rest.responses.GetViewResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.rest.responses.ListPartitionsResponse; import org.apache.paimon.rest.responses.ListTablesResponse; +import org.apache.paimon.rest.responses.ListViewsResponse; import org.apache.paimon.types.DataField; import org.apache.paimon.types.DataTypes; import org.apache.paimon.types.IntType; @@ -172,11 +175,12 @@ public void dataFieldParseTest() throws Exception { @Test public void renameTableRequestParseTest() throws Exception { - RenameTableRequest request = MockRESTMessage.renameRequest("t2"); + RenameTableRequest request = MockRESTMessage.renameRequest("t1", "t2"); String requestStr = OBJECT_MAPPER.writeValueAsString(request); RenameTableRequest parseData = OBJECT_MAPPER.readValue(requestStr, RenameTableRequest.class); - assertEquals(request.getNewIdentifier(), parseData.getNewIdentifier()); + assertEquals(request.getSource(), parseData.getSource()); + assertEquals(request.getDestination(), parseData.getDestination()); } @Test @@ -242,4 +246,31 @@ public void alterPartitionsRequestParseTest() throws Exception { OBJECT_MAPPER.readValue(requestStr, AlterPartitionsRequest.class); assertEquals(request.getPartitions(), parseData.getPartitions()); } + + @Test + public void createViewRequestParseTest() throws Exception { + CreateViewRequest request = MockRESTMessage.createViewRequest("t1"); + String requestStr = OBJECT_MAPPER.writeValueAsString(request); + CreateViewRequest parseData = OBJECT_MAPPER.readValue(requestStr, CreateViewRequest.class); + assertEquals(request.getIdentifier(), parseData.getIdentifier()); + assertEquals(request.getSchema(), parseData.getSchema()); + } + + @Test + public void getViewResponseParseTest() throws Exception { + GetViewResponse response = MockRESTMessage.getViewResponse(); + String responseStr = OBJECT_MAPPER.writeValueAsString(response); + GetViewResponse parseData = OBJECT_MAPPER.readValue(responseStr, GetViewResponse.class); + assertEquals(response.getId(), parseData.getId()); + assertEquals(response.getName(), parseData.getName()); + assertEquals(response.getSchema(), parseData.getSchema()); + } + + @Test + public void listViewsResponseParseTest() throws Exception { + ListViewsResponse response = MockRESTMessage.listViewsResponse(); + String responseStr = OBJECT_MAPPER.writeValueAsString(response); + ListViewsResponse parseData = OBJECT_MAPPER.readValue(responseStr, ListViewsResponse.class); + assertEquals(response.getViews(), parseData.getViews()); + } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/TestRESTCatalog.java b/paimon-core/src/test/java/org/apache/paimon/rest/TestRESTCatalog.java index 4f150493110b..f56030252bfd 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/TestRESTCatalog.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/TestRESTCatalog.java @@ -32,6 +32,7 @@ import org.apache.paimon.schema.Schema; import org.apache.paimon.schema.SchemaChange; import org.apache.paimon.schema.TableSchema; +import org.apache.paimon.view.View; import java.io.IOException; import java.io.UncheckedIOException; @@ -46,6 +47,7 @@ public class TestRESTCatalog extends FileSystemCatalog { public Map tableFullName2Schema = new HashMap(); public Map> tableFullName2Partitions = new HashMap>(); + public final Map viewFullName2View = new HashMap(); public TestRESTCatalog(FileIO fileIO, Path warehouse, Options options) { super(fileIO, warehouse, options); @@ -130,6 +132,61 @@ public List listPartitions(Identifier identifier) throws TableNotExis return tableFullName2Partitions.get(identifier.getFullName()); } + @Override + public View getView(Identifier identifier) throws ViewNotExistException { + if (viewFullName2View.containsKey(identifier.getFullName())) { + return viewFullName2View.get(identifier.getFullName()); + } + throw new ViewNotExistException(identifier); + } + + @Override + public void dropView(Identifier identifier, boolean ignoreIfNotExists) + throws ViewNotExistException { + if (viewFullName2View.containsKey(identifier.getFullName())) { + viewFullName2View.remove(identifier.getFullName()); + } + if (!ignoreIfNotExists) { + throw new ViewNotExistException(identifier); + } + } + + @Override + public void createView(Identifier identifier, View view, boolean ignoreIfExists) + throws ViewAlreadyExistException, DatabaseNotExistException { + getDatabase(identifier.getDatabaseName()); + if (viewFullName2View.containsKey(identifier.getFullName()) && !ignoreIfExists) { + throw new ViewAlreadyExistException(identifier); + } + viewFullName2View.put(identifier.getFullName(), view); + } + + @Override + public List listViews(String databaseName) throws DatabaseNotExistException { + getDatabase(databaseName); + return viewFullName2View.keySet().stream() + .map(v -> Identifier.fromString(v)) + .filter(identifier -> identifier.getDatabaseName().equals(databaseName)) + .map(identifier -> identifier.getTableName()) + .collect(Collectors.toList()); + } + + @Override + public void renameView(Identifier fromView, Identifier toView, boolean ignoreIfNotExists) + throws ViewNotExistException, ViewAlreadyExistException { + if (!viewFullName2View.containsKey(fromView.getFullName()) && !ignoreIfNotExists) { + throw new ViewNotExistException(fromView); + } + if (viewFullName2View.containsKey(toView.getFullName())) { + throw new ViewAlreadyExistException(toView); + } + if (viewFullName2View.containsKey(fromView.getFullName())) { + View view = viewFullName2View.get(fromView.getFullName()); + viewFullName2View.remove(fromView.getFullName()); + viewFullName2View.put(toView.getFullName(), view); + } + } + @Override protected List listTablesImpl(String databaseName) { List tables = super.listTablesImpl(databaseName); diff --git a/paimon-open-api/rest-catalog-open-api.yaml b/paimon-open-api/rest-catalog-open-api.yaml index e60bb6617767..02ea7de8d0df 100644 --- a/paimon-open-api/rest-catalog-open-api.yaml +++ b/paimon-open-api/rest-catalog-open-api.yaml @@ -240,11 +240,7 @@ paths: $ref: '#/components/schemas/CreateTableRequest' responses: "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/GetTableResponse' + description: Success, no content "500": description: Internal Server Error /v1/{prefix}/databases/{database}/tables/{table}: @@ -312,11 +308,7 @@ paths: $ref: '#/components/schemas/AlterTableRequest' responses: "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/GetTableResponse' + description: Success, no content "404": description: Resource not found content: @@ -358,7 +350,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' "500": description: Internal Server Error - /v1/{prefix}/databases/{database}/tables/{table}/rename: + /v1/{prefix}/databases/{database}/tables/rename: post: tags: - table @@ -375,11 +367,6 @@ paths: required: true schema: type: string - - name: table - in: path - required: true - schema: - type: string requestBody: content: application/json: @@ -387,11 +374,7 @@ paths: $ref: '#/components/schemas/RenameTableRequest' responses: "200": - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/GetTableResponse' + description: Success, no content "404": description: Resource not found content: @@ -406,7 +389,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' "500": description: Internal Server Error - /v1/{prefix}/databases/{database}/tables/{table}/commit: + /v1/{prefix}/databases/{database}/tables/commit: post: tags: - table @@ -423,11 +406,6 @@ paths: required: true schema: type: string - - name: table - in: path - required: true - schema: - type: string requestBody: content: application/json: @@ -642,6 +620,172 @@ paths: $ref: '#/components/schemas/ErrorResponse' "500": description: Internal Server Error + /v1/{prefix}/databases/{database}/views: + get: + tags: + - view + summary: List views + operationId: listViews + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListViewsResponse' + "500": + description: Internal Server Error + post: + tags: + - view + summary: Create view + operationId: createView + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateViewRequest' + responses: + "200": + description: Success, no content + "404": + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: Internal Server Error + /v1/{prefix}/databases/{database}/views/{view}: + get: + tags: + - view + summary: Get view + operationId: getView + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + - name: view + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetViewResponse' + "404": + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: Internal Server Error + delete: + tags: + - view + summary: Drop view + operationId: dropView + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + - name: view + in: path + required: true + schema: + type: string + responses: + "200": + description: Success, no content + "404": + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: Internal Server Error + /v1/{prefix}/databases/{database}/views/rename: + post: + tags: + - view + summary: Rename view + operationId: renameView + parameters: + - name: prefix + in: path + required: true + schema: + type: string + - name: database + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RenameTableRequest' + responses: + "200": + description: Success, no content + "404": + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "409": + description: Resource has exist + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + "500": + description: Internal Server Error components: schemas: CreateDatabaseRequest: @@ -709,6 +853,13 @@ components: $ref: '#/components/schemas/Identifier' schema: $ref: '#/components/schemas/Schema' + CreateViewRequest: + type: object + properties: + identifier: + $ref: '#/components/schemas/Identifier' + schema: + $ref: '#/components/schemas/ViewSchema' DataField: type: object properties: @@ -767,7 +918,9 @@ components: pattern: ^ROW.* example: ROW fields: - $ref: '#/components/schemas/DataField' + type: array + items: + $ref: '#/components/schemas/DataField' Identifier: type: object properties: @@ -946,7 +1099,9 @@ components: RenameTableRequest: type: object properties: - newIdentifier: + source: + $ref: '#/components/schemas/Identifier' + destination: $ref: '#/components/schemas/Identifier' CommitTableRequest: type: object @@ -960,8 +1115,52 @@ components: properties: version: type: integer + format: int32 + nullable: true id: type: integer + format: int64 + schemaId: + type: integer + format: int64 + baseManifestList: + type: string + deltaManifestList: + type: string + changelogManifestList: + type: string + nullable: true + indexManifest: + type: string + commitUser: + type: string + commitIdentifier: + type: string + commitKind: + type: string + enum: ["APPEND", "COMPACT", "OVERWRITE", "ANALYZE"] + timeMillis: + type: integer + format: int64 + logOffsets: + type: object + additionalProperties: + type: integer + format: int64 + totalRecordCount: + type: integer + format: int64 + deltaRecordCount: + type: integer + format: int64 + changelogRecordCount: + type: integer + format: int64 + watermark: + type: integer + format: int64 + statistics: + type: string CommitTableResponse: type: object properties: @@ -1043,6 +1242,35 @@ components: type: array items: $ref: '#/components/schemas/Partition' + GetViewResponse: + type: object + properties: + id: + type: string + name: + type: string + schema: + $ref: '#/components/schemas/ViewSchema' + ListViewsResponse: + type: object + properties: + views: + type: array + items: + type: string + ViewSchema: + type: object + properties: + rowType: + $ref: '#/components/schemas/RowType' + options: + type: object + additionalProperties: + type: string + comment: + type: string + query: + type: string Partition: type: object properties: diff --git a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java index 745a1e9ffd34..e657407a47c3 100644 --- a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java +++ b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java @@ -27,6 +27,7 @@ import org.apache.paimon.rest.requests.CreateDatabaseRequest; import org.apache.paimon.rest.requests.CreatePartitionsRequest; import org.apache.paimon.rest.requests.CreateTableRequest; +import org.apache.paimon.rest.requests.CreateViewRequest; import org.apache.paimon.rest.requests.DropPartitionsRequest; import org.apache.paimon.rest.requests.MarkDonePartitionsRequest; import org.apache.paimon.rest.requests.RenameTableRequest; @@ -37,9 +38,15 @@ import org.apache.paimon.rest.responses.ErrorResponse; import org.apache.paimon.rest.responses.GetDatabaseResponse; import org.apache.paimon.rest.responses.GetTableResponse; +import org.apache.paimon.rest.responses.GetViewResponse; import org.apache.paimon.rest.responses.ListDatabasesResponse; import org.apache.paimon.rest.responses.ListPartitionsResponse; import org.apache.paimon.rest.responses.ListTablesResponse; +import org.apache.paimon.rest.responses.ListViewsResponse; +import org.apache.paimon.types.DataField; +import org.apache.paimon.types.IntType; +import org.apache.paimon.types.RowType; +import org.apache.paimon.view.ViewSchema; import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList; import org.apache.paimon.shade.guava30.com.google.common.collect.Lists; @@ -57,7 +64,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -253,37 +263,22 @@ public GetTableResponse getTable( summary = "Create table", tags = {"table"}) @ApiResponses({ - @ApiResponse( - responseCode = "200", - content = {@Content(schema = @Schema(implementation = GetTableResponse.class))}), + @ApiResponse(responseCode = "200", description = "Success, no content"), @ApiResponse( responseCode = "500", content = {@Content(schema = @Schema())}) }) @PostMapping("/v1/{prefix}/databases/{database}/tables") - public GetTableResponse createTable( + public void createTable( @PathVariable String prefix, @PathVariable String database, - @RequestBody CreateTableRequest request) { - return new GetTableResponse( - UUID.randomUUID().toString(), - "", - 1, - new org.apache.paimon.schema.Schema( - ImmutableList.of(), - ImmutableList.of(), - ImmutableList.of(), - new HashMap<>(), - "comment")); - } + @RequestBody CreateTableRequest request) {} @Operation( summary = "Alter table", tags = {"table"}) @ApiResponses({ - @ApiResponse( - responseCode = "200", - content = {@Content(schema = @Schema(implementation = GetTableResponse.class))}), + @ApiResponse(responseCode = "200", description = "Success, no content"), @ApiResponse( responseCode = "404", description = "Resource not found", @@ -293,22 +288,11 @@ public GetTableResponse createTable( content = {@Content(schema = @Schema())}) }) @PostMapping("/v1/{prefix}/databases/{database}/tables/{table}") - public GetTableResponse alterTable( + public void alterTable( @PathVariable String prefix, @PathVariable String database, @PathVariable String table, - @RequestBody AlterTableRequest request) { - return new GetTableResponse( - UUID.randomUUID().toString(), - "", - 1, - new org.apache.paimon.schema.Schema( - ImmutableList.of(), - ImmutableList.of(), - ImmutableList.of(), - new HashMap<>(), - "comment")); - } + @RequestBody AlterTableRequest request) {} @Operation( summary = "Drop table", @@ -333,9 +317,7 @@ public void dropTable( summary = "Rename table", tags = {"table"}) @ApiResponses({ - @ApiResponse( - responseCode = "200", - content = {@Content(schema = @Schema(implementation = GetTableResponse.class))}), + @ApiResponse(responseCode = "200", description = "Success, no content"), @ApiResponse( responseCode = "404", description = "Resource not found", @@ -344,23 +326,11 @@ public void dropTable( responseCode = "500", content = {@Content(schema = @Schema())}) }) - @PostMapping("/v1/{prefix}/databases/{database}/tables/{table}/rename") - public GetTableResponse renameTable( + @PostMapping("/v1/{prefix}/databases/{database}/tables/rename") + public void renameTable( @PathVariable String prefix, @PathVariable String database, - @PathVariable String table, - @RequestBody RenameTableRequest request) { - return new GetTableResponse( - UUID.randomUUID().toString(), - "", - 1, - new org.apache.paimon.schema.Schema( - ImmutableList.of(), - ImmutableList.of(), - ImmutableList.of(), - new HashMap<>(), - "comment")); - } + @RequestBody RenameTableRequest request) {} @Operation( summary = "Commit table", @@ -377,11 +347,10 @@ public GetTableResponse renameTable( responseCode = "500", content = {@Content(schema = @Schema())}) }) - @PostMapping("/v1/{prefix}/databases/{database}/tables/{table}/commit") + @PostMapping("/v1/{prefix}/databases/{database}/tables/commit") public CommitTableResponse commitTable( @PathVariable String prefix, @PathVariable String database, - @PathVariable String table, @RequestBody CommitTableRequest request) { return new CommitTableResponse(true); } @@ -493,4 +462,112 @@ public void markDonePartitions( @PathVariable String database, @PathVariable String table, @RequestBody MarkDonePartitionsRequest request) {} + + @Operation( + summary = "List views", + tags = {"view"}) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + content = {@Content(schema = @Schema(implementation = ListViewsResponse.class))}), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @GetMapping("/v1/{prefix}/databases/{database}/views") + public ListViewsResponse listViews(@PathVariable String prefix, @PathVariable String database) { + return new ListViewsResponse(ImmutableList.of("view1")); + } + + @Operation( + summary = "create view", + tags = {"view"}) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success, no content"), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @PostMapping("/v1/{prefix}/databases/{database}/views") + public void createView( + @PathVariable String prefix, + @PathVariable String database, + @RequestBody CreateViewRequest request) {} + + @Operation( + summary = "Get view", + tags = {"view"}) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + content = {@Content(schema = @Schema(implementation = GetViewResponse.class))}), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @GetMapping("/v1/{prefix}/databases/{database}/views/{view}") + public GetViewResponse getView( + @PathVariable String prefix, @PathVariable String database, @PathVariable String view) { + List fields = + Arrays.asList( + new DataField(0, "f0", new IntType()), + new DataField(1, "f1", new IntType())); + ViewSchema schema = + new ViewSchema( + new RowType(fields), + Collections.singletonMap("pt", "1"), + "comment", + "select * from t1"); + return new GetViewResponse("id", "name", schema); + } + + @Operation( + summary = "Rename view", + tags = {"view"}) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success, no content"), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @PostMapping("/v1/{prefix}/databases/{database}/views/rename") + public void renameView( + @PathVariable String prefix, + @PathVariable String database, + @RequestBody RenameTableRequest request) {} + + @Operation( + summary = "Drop view", + tags = {"view"}) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success, no content"), + @ApiResponse( + responseCode = "404", + description = "Resource not found", + content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}), + @ApiResponse( + responseCode = "500", + content = {@Content(schema = @Schema())}) + }) + @DeleteMapping("/v1/{prefix}/databases/{database}/views/{view}") + public void dropView( + @PathVariable String prefix, + @PathVariable String database, + @PathVariable String view) {} }