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

lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 3b08450bc [core] Introduce View Support to HiveCatalog (#4340)
3b08450bc is described below

commit 3b08450bcc0ccefdc795710c575f94821bb1415b
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon Oct 28 17:22:57 2024 +0800

    [core] Introduce View Support to HiveCatalog (#4340)
---
 .../java/org/apache/paimon/catalog/Catalog.java    | 106 +++++++++++++++
 .../org/apache/paimon/catalog/DelegateCatalog.java |  33 +++++
 .../src/main/java/org/apache/paimon/view/View.java |  49 +++++++
 .../main/java/org/apache/paimon/view/ViewImpl.java | 110 ++++++++++++++++
 .../org/apache/paimon/catalog/CatalogTestBase.java |  64 +++++++++
 .../java/org/apache/paimon/flink/FlinkCatalog.java |  97 ++++++++++++--
 .../java/org/apache/paimon/hive/HiveCatalog.java   | 145 ++++++++++++++++++++-
 ...veFormatTableUtils.java => HiveTableUtils.java} |  11 +-
 .../org/apache/paimon/hive/HiveCatalogTest.java    |   5 +
 .../apache/paimon/hive/HiveCatalogITCaseBase.java  |  37 ++++--
 10 files changed, 629 insertions(+), 28 deletions(-)

diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
index 9f069dc36..7fbb777e3 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
@@ -25,6 +25,7 @@ import org.apache.paimon.metastore.MetastoreClient;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
 import org.apache.paimon.table.Table;
+import org.apache.paimon.view.View;
 
 import java.io.Serializable;
 import java.util.Arrays;
@@ -299,6 +300,69 @@ public interface Catalog extends AutoCloseable {
         alterTable(identifier, Collections.singletonList(change), 
ignoreIfNotExists);
     }
 
+    /**
+     * Check if a view exists in this catalog.
+     *
+     * @param identifier Path of the view
+     * @return true if the given view exists in the catalog false otherwise
+     */
+    default boolean viewExists(Identifier identifier) {
+        try {
+            return getView(identifier) != null;
+        } catch (ViewNotExistException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Return a {@link View} identified by the given {@link Identifier}.
+     *
+     * @param identifier Path of the view
+     * @return The requested view
+     * @throws ViewNotExistException if the target does not exist
+     */
+    default View getView(Identifier identifier) throws ViewNotExistException {
+        throw new ViewNotExistException(identifier);
+    }
+
+    /**
+     * Drop a view.
+     *
+     * @param identifier Path of the view to be dropped
+     * @param ignoreIfNotExists Flag to specify behavior when the view does 
not exist: if set to
+     *     false, throw an exception, if set to true, do nothing.
+     * @throws ViewNotExistException if the view does not exist
+     */
+    default void dropView(Identifier identifier, boolean ignoreIfNotExists)
+            throws ViewNotExistException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Create a new view.
+     *
+     * @param identifier path of the view to be created
+     * @param view the view definition
+     * @param ignoreIfExists flag to specify behavior when a view already 
exists at the given path:
+     *     if set to false, it throws a ViewAlreadyExistException, if set to 
true, do nothing.
+     * @throws ViewAlreadyExistException if view already exists and 
ignoreIfExists is false
+     * @throws DatabaseNotExistException if the database in identifier doesn't 
exist
+     */
+    default void createView(Identifier identifier, View view, boolean 
ignoreIfExists)
+            throws ViewAlreadyExistException, DatabaseNotExistException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Get names of all views under this database. An empty list is returned 
if none exists.
+     *
+     * @return a list of the names of all views in this database
+     * @throws DatabaseNotExistException if the database does not exist
+     */
+    default List<String> listViews(String databaseName) throws 
DatabaseNotExistException {
+        return Collections.emptyList();
+    }
+
     /** Return a boolean that indicates whether this catalog allow upper case. 
*/
     boolean allowUpperCase();
 
@@ -532,6 +596,48 @@ public interface Catalog extends AutoCloseable {
         }
     }
 
+    /** Exception for trying to create a view that already exists. */
+    class ViewAlreadyExistException extends Exception {
+
+        private static final String MSG = "View %s already exists.";
+
+        private final Identifier identifier;
+
+        public ViewAlreadyExistException(Identifier identifier) {
+            this(identifier, null);
+        }
+
+        public ViewAlreadyExistException(Identifier identifier, Throwable 
cause) {
+            super(String.format(MSG, identifier.getFullName()), cause);
+            this.identifier = identifier;
+        }
+
+        public Identifier identifier() {
+            return identifier;
+        }
+    }
+
+    /** Exception for trying to operate on a view that doesn't exist. */
+    class ViewNotExistException extends Exception {
+
+        private static final String MSG = "View %s does not exist.";
+
+        private final Identifier identifier;
+
+        public ViewNotExistException(Identifier identifier) {
+            this(identifier, null);
+        }
+
+        public ViewNotExistException(Identifier identifier, Throwable cause) {
+            super(String.format(MSG, identifier.getFullName()), cause);
+            this.identifier = identifier;
+        }
+
+        public Identifier identifier() {
+            return identifier;
+        }
+    }
+
     /** Loader of {@link Catalog}. */
     @FunctionalInterface
     interface Loader extends Serializable {
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
index 814de16d6..d05e100b2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
@@ -24,6 +24,7 @@ import org.apache.paimon.metastore.MetastoreClient;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
 import org.apache.paimon.table.Table;
+import org.apache.paimon.view.View;
 
 import java.util.List;
 import java.util.Map;
@@ -135,6 +136,38 @@ public class DelegateCatalog implements Catalog {
         return wrapped.getTable(identifier);
     }
 
+    @Override
+    public boolean tableExists(Identifier identifier) {
+        return wrapped.tableExists(identifier);
+    }
+
+    @Override
+    public boolean viewExists(Identifier identifier) {
+        return wrapped.viewExists(identifier);
+    }
+
+    @Override
+    public View getView(Identifier identifier) throws ViewNotExistException {
+        return wrapped.getView(identifier);
+    }
+
+    @Override
+    public void dropView(Identifier identifier, boolean ignoreIfNotExists)
+            throws ViewNotExistException {
+        wrapped.dropView(identifier, ignoreIfNotExists);
+    }
+
+    @Override
+    public void createView(Identifier identifier, View view, boolean 
ignoreIfExists)
+            throws ViewAlreadyExistException, DatabaseNotExistException {
+        wrapped.createView(identifier, view, ignoreIfExists);
+    }
+
+    @Override
+    public List<String> listViews(String databaseName) throws 
DatabaseNotExistException {
+        return wrapped.listViews(databaseName);
+    }
+
     @Override
     public Path getTableLocation(Identifier identifier) {
         return wrapped.getTableLocation(identifier);
diff --git a/paimon-core/src/main/java/org/apache/paimon/view/View.java 
b/paimon-core/src/main/java/org/apache/paimon/view/View.java
new file mode 100644
index 000000000..87f567642
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/view/View.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.paimon.view;
+
+import org.apache.paimon.types.RowType;
+
+import java.util.Map;
+import java.util.Optional;
+
+/** Interface for view definition. */
+public interface View {
+
+    /** A name to identify this view. */
+    String name();
+
+    /** Full name (including database) to identify this view. */
+    String fullName();
+
+    /** Returns the row type of this view. */
+    RowType rowType();
+
+    /** Returns the view representation. */
+    String query();
+
+    /** Optional comment of this view. */
+    Optional<String> comment();
+
+    /** Options of this view. */
+    Map<String, String> options();
+
+    /** Copy this view with adding dynamic options. */
+    View copy(Map<String, String> dynamicOptions);
+}
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
new file mode 100644
index 000000000..1cd48d4ce
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/view/ViewImpl.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.paimon.view;
+
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.types.RowType;
+
+import javax.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/** Implementation of {@link View}. */
+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<String, String> options;
+
+    public ViewImpl(
+            Identifier identifier,
+            RowType rowType,
+            String query,
+            @Nullable String comment,
+            Map<String, String> options) {
+        this.identifier = identifier;
+        this.rowType = rowType;
+        this.query = query;
+        this.comment = comment;
+        this.options = options;
+    }
+
+    @Override
+    public String name() {
+        return identifier.getObjectName();
+    }
+
+    @Override
+    public String fullName() {
+        return identifier.getFullName();
+    }
+
+    @Override
+    public RowType rowType() {
+        return rowType;
+    }
+
+    @Override
+    public String query() {
+        return query;
+    }
+
+    @Override
+    public Optional<String> comment() {
+        return Optional.ofNullable(comment);
+    }
+
+    @Override
+    public Map<String, String> options() {
+        return options;
+    }
+
+    @Override
+    public View copy(Map<String, String> dynamicOptions) {
+        Map<String, String> newOptions = new HashMap<>(options);
+        newOptions.putAll(dynamicOptions);
+        return new ViewImpl(identifier, rowType, query, comment, newOptions);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        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);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(identifier, rowType, query, comment, options);
+    }
+}
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 c4470b228..9bb643c9e 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
@@ -29,6 +29,8 @@ import org.apache.paimon.table.Table;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.view.View;
+import org.apache.paimon.view.ViewImpl;
 
 import org.apache.paimon.shade.guava30.com.google.common.collect.Lists;
 import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
@@ -40,7 +42,9 @@ import org.junit.jupiter.api.io.TempDir;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import static 
org.apache.paimon.testutils.assertj.PaimonAssertions.anyCauseMatches;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -831,4 +835,64 @@ public abstract class CatalogTestBase {
         table = catalog.getTable(identifier);
         assertThat(table.comment().isPresent()).isFalse();
     }
+
+    protected boolean supportsView() {
+        return false;
+    }
+
+    @Test
+    public void testView() throws Exception {
+        if (!supportsView()) {
+            return;
+        }
+
+        Identifier identifier = new Identifier("view_db", "my_view");
+        RowType rowType =
+                RowType.builder()
+                        .field("str", DataTypes.STRING())
+                        .field("int", DataTypes.INT())
+                        .build();
+        String query = "SELECT * FROM OTHER_TABLE";
+        String comment = "it is my view";
+        Map<String, String> options = new HashMap<>();
+        options.put("key1", "v1");
+        options.put("key2", "v2");
+        View view = new ViewImpl(identifier, rowType, query, comment, options);
+
+        assertThatThrownBy(() -> catalog.createView(identifier, view, false))
+                .isInstanceOf(Catalog.DatabaseNotExistException.class);
+
+        assertThatThrownBy(() -> 
catalog.listViews(identifier.getDatabaseName()))
+                .isInstanceOf(Catalog.DatabaseNotExistException.class);
+
+        catalog.createDatabase(identifier.getDatabaseName(), false);
+
+        assertThatThrownBy(() -> catalog.getView(identifier))
+                .isInstanceOf(Catalog.ViewNotExistException.class);
+
+        catalog.createView(identifier, view, false);
+
+        assertThat(catalog.viewExists(identifier)).isTrue();
+
+        View catalogView = catalog.getView(identifier);
+        assertThat(catalogView.fullName()).isEqualTo(view.fullName());
+        assertThat(catalogView.rowType()).isEqualTo(view.rowType());
+        assertThat(catalogView.query()).isEqualTo(view.query());
+        assertThat(catalogView.comment()).isEqualTo(view.comment());
+        assertThat(catalogView.options()).containsAllEntriesOf(view.options());
+
+        List<String> views = catalog.listViews(identifier.getDatabaseName());
+        assertThat(views).containsOnly(identifier.getObjectName());
+
+        catalog.createView(identifier, view, true);
+        assertThatThrownBy(() -> catalog.createView(identifier, view, false))
+                .isInstanceOf(Catalog.ViewAlreadyExistException.class);
+
+        catalog.dropView(identifier, false);
+        assertThat(catalog.viewExists(identifier)).isFalse();
+
+        catalog.dropView(identifier, true);
+        assertThatThrownBy(() -> catalog.dropView(identifier, false))
+                .isInstanceOf(Catalog.ViewNotExistException.class);
+    }
 }
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java
index c1e7db6b2..ad624b560 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/FlinkCatalog.java
@@ -41,6 +41,8 @@ import org.apache.paimon.utils.FileStorePathFactory;
 import org.apache.paimon.utils.InternalRowPartitionComputer;
 import org.apache.paimon.utils.Preconditions;
 import org.apache.paimon.utils.StringUtils;
+import org.apache.paimon.view.View;
+import org.apache.paimon.view.ViewImpl;
 
 import org.apache.flink.table.api.TableSchema;
 import org.apache.flink.table.catalog.AbstractCatalog;
@@ -53,10 +55,12 @@ import org.apache.flink.table.catalog.CatalogPartition;
 import org.apache.flink.table.catalog.CatalogPartitionImpl;
 import org.apache.flink.table.catalog.CatalogPartitionSpec;
 import org.apache.flink.table.catalog.CatalogTable;
+import org.apache.flink.table.catalog.CatalogView;
 import org.apache.flink.table.catalog.Column;
 import org.apache.flink.table.catalog.IntervalFreshness;
 import org.apache.flink.table.catalog.ObjectPath;
 import org.apache.flink.table.catalog.ResolvedCatalogBaseTable;
+import org.apache.flink.table.catalog.ResolvedCatalogView;
 import org.apache.flink.table.catalog.ResolvedSchema;
 import org.apache.flink.table.catalog.TableChange;
 import org.apache.flink.table.catalog.TableChange.AddColumn;
@@ -284,6 +288,11 @@ public class FlinkCatalog extends AbstractCatalog {
         try {
             table = catalog.getTable(toIdentifier(tablePath));
         } catch (Catalog.TableNotExistException e) {
+            Optional<CatalogBaseTable> view = getView(tablePath, timestamp);
+            if (view.isPresent()) {
+                return view.get();
+            }
+
             throw new TableNotExistException(getName(), tablePath);
         }
 
@@ -309,17 +318,53 @@ public class FlinkCatalog extends AbstractCatalog {
         }
     }
 
+    private Optional<CatalogBaseTable> getView(ObjectPath tablePath, @Nullable 
Long timestamp) {
+        View view;
+        try {
+            view = catalog.getView(toIdentifier(tablePath));
+        } catch (Catalog.ViewNotExistException e) {
+            return Optional.empty();
+        }
+
+        if (timestamp != null) {
+            throw new UnsupportedOperationException(
+                    String.format("View %s does not support time travel.", 
tablePath));
+        }
+
+        org.apache.flink.table.api.Schema schema =
+                org.apache.flink.table.api.Schema.newBuilder()
+                        
.fromRowDataType(fromLogicalToDataType(toLogicalType(view.rowType())))
+                        .build();
+        return Optional.of(
+                CatalogView.of(
+                        schema,
+                        view.comment().orElse(null),
+                        view.query(),
+                        view.query(),
+                        view.options()));
+    }
+
     @Override
     public boolean tableExists(ObjectPath tablePath) throws CatalogException {
-        return catalog.tableExists(toIdentifier(tablePath));
+        Identifier identifier = toIdentifier(tablePath);
+        return catalog.tableExists(identifier) || 
catalog.viewExists(identifier);
     }
 
     @Override
     public void dropTable(ObjectPath tablePath, boolean ignoreIfNotExists)
             throws TableNotExistException, CatalogException {
         Identifier identifier = toIdentifier(tablePath);
-        Table table = null;
+        if (catalog.viewExists(identifier)) {
+            try {
+                catalog.dropView(identifier, ignoreIfNotExists);
+                return;
+            } catch (Catalog.ViewNotExistException e) {
+                throw new RuntimeException("Unexpected exception.", e);
+            }
+        }
+
         try {
+            Table table = null;
             if (logStoreAutoRegister && catalog.tableExists(identifier)) {
                 table = catalog.getTable(identifier);
             }
@@ -335,18 +380,17 @@ public class FlinkCatalog extends AbstractCatalog {
     @Override
     public void createTable(ObjectPath tablePath, CatalogBaseTable table, 
boolean ignoreIfExists)
             throws TableAlreadyExistException, DatabaseNotExistException, 
CatalogException {
-        if (!(table instanceof CatalogTable || table instanceof 
CatalogMaterializedTable)) {
-            throw new UnsupportedOperationException(
-                    "Only support CatalogTable and CatalogMaterializedTable, 
but is: "
-                            + table.getClass());
-        }
-
         if (Objects.equals(getDefaultDatabase(), tablePath.getDatabaseName())
                 && disableCreateTableInDefaultDatabase) {
             throw new UnsupportedOperationException(
                     "Creating table in default database is disabled, please 
specify a database name.");
         }
 
+        if (table instanceof CatalogView) {
+            createView(tablePath, (ResolvedCatalogView) table, ignoreIfExists);
+            return;
+        }
+
         Identifier identifier = toIdentifier(tablePath);
         // the returned value of "table.getOptions" may be unmodifiable (for 
example from
         // TableDescriptor)
@@ -372,6 +416,34 @@ public class FlinkCatalog extends AbstractCatalog {
         }
     }
 
+    private void createView(ObjectPath tablePath, ResolvedCatalogView table, 
boolean ignoreIfExists)
+            throws TableAlreadyExistException, DatabaseNotExistException {
+        Identifier identifier = toIdentifier(tablePath);
+        org.apache.paimon.types.RowType.Builder builder = 
org.apache.paimon.types.RowType.builder();
+        table.getResolvedSchema()
+                .getColumns()
+                .forEach(
+                        column ->
+                                builder.field(
+                                        column.getName(),
+                                        
toDataType(column.getDataType().getLogicalType()),
+                                        column.getComment().orElse(null)));
+        View view =
+                new ViewImpl(
+                        identifier,
+                        builder.build(),
+                        table.getOriginalQuery(),
+                        table.getComment(),
+                        table.getOptions());
+        try {
+            catalog.createView(identifier, view, ignoreIfExists);
+        } catch (Catalog.ViewAlreadyExistException e) {
+            throw new TableAlreadyExistException(getName(), tablePath);
+        } catch (Catalog.DatabaseNotExistException e) {
+            throw new DatabaseNotExistException(getName(), 
tablePath.getDatabaseName());
+        }
+    }
+
     private static void fillOptionsForMaterializedTable(
             CatalogMaterializedTable mt, Map<String, String> options) {
         Options mtOptions = new Options();
@@ -1025,8 +1097,13 @@ public class FlinkCatalog extends AbstractCatalog {
     }
 
     @Override
-    public final List<String> listViews(String databaseName) throws 
CatalogException {
-        return Collections.emptyList();
+    public final List<String> listViews(String databaseName)
+            throws DatabaseNotExistException, CatalogException {
+        try {
+            return catalog.listViews(databaseName);
+        } catch (Catalog.DatabaseNotExistException e) {
+            throw new DatabaseNotExistException(getName(), databaseName);
+        }
     }
 
     @Override
diff --git 
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
 
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
index d4f886247..657bce8a5 100644
--- 
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
+++ 
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveCatalog.java
@@ -46,12 +46,15 @@ import org.apache.paimon.table.FormatTable;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.view.View;
+import org.apache.paimon.view.ViewImpl;
 
 import org.apache.flink.table.hive.LegacyHiveClasses;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.hadoop.hive.metastore.IMetaStoreClient;
+import org.apache.hadoop.hive.metastore.TableType;
 import org.apache.hadoop.hive.metastore.api.Database;
 import org.apache.hadoop.hive.metastore.api.FieldSchema;
 import org.apache.hadoop.hive.metastore.api.NoSuchObjectException;
@@ -440,7 +443,7 @@ public class HiveCatalog extends AbstractCatalog {
 
         if (formatTableEnabled()) {
             try {
-                HiveFormatTableUtils.convertToFormatTable(table);
+                HiveTableUtils.convertToFormatTable(table);
                 return true;
             } catch (UnsupportedOperationException e) {
                 return false;
@@ -468,6 +471,139 @@ public class HiveCatalog extends AbstractCatalog {
                 .orElseThrow(() -> new TableNotExistException(identifier));
     }
 
+    @Override
+    public View getView(Identifier identifier) throws ViewNotExistException {
+        Table table;
+        try {
+            table =
+                    clients.run(
+                            client ->
+                                    client.getTable(
+                                            identifier.getDatabaseName(),
+                                            identifier.getTableName()));
+        } catch (NoSuchObjectException e) {
+            throw new ViewNotExistException(identifier);
+        } catch (TException e) {
+            throw new RuntimeException(e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(e);
+        }
+
+        if (!isView(table)) {
+            throw new ViewNotExistException(identifier);
+        }
+
+        RowType rowType = HiveTableUtils.createRowType(table);
+        Map<String, String> options = new HashMap<>(table.getParameters());
+        String comment = options.remove(COMMENT_PROP);
+        return new ViewImpl(identifier, rowType, table.getViewExpandedText(), 
comment, options);
+    }
+
+    @Override
+    public void createView(Identifier identifier, View view, boolean 
ignoreIfExists)
+            throws ViewAlreadyExistException, DatabaseNotExistException {
+        if (!databaseExists(identifier.getDatabaseName())) {
+            throw new DatabaseNotExistException(identifier.getDatabaseName());
+        }
+
+        try {
+            getView(identifier);
+            if (ignoreIfExists) {
+                return;
+            }
+            throw new ViewAlreadyExistException(identifier);
+        } catch (ViewNotExistException ignored) {
+        }
+
+        Table hiveTable =
+                org.apache.hadoop.hive.ql.metadata.Table.getEmptyTable(
+                        identifier.getDatabaseName(), 
identifier.getObjectName());
+        hiveTable.setCreateTime((int) (System.currentTimeMillis() / 1000));
+
+        Map<String, String> properties = new HashMap<>(view.options());
+        // Table comment
+        if (view.comment().isPresent()) {
+            properties.put(COMMENT_PROP, view.comment().get());
+        }
+        hiveTable.setParameters(properties);
+        hiveTable.setPartitionKeys(new ArrayList<>());
+        hiveTable.setViewOriginalText(view.query());
+        hiveTable.setViewExpandedText(view.query());
+        hiveTable.setTableType(TableType.VIRTUAL_VIEW.name());
+
+        StorageDescriptor sd = hiveTable.getSd();
+        List<FieldSchema> columns =
+                view.rowType().getFields().stream()
+                        .map(this::convertToFieldSchema)
+                        .collect(Collectors.toList());
+        sd.setCols(columns);
+
+        try {
+            clients.execute(client -> client.createTable(hiveTable));
+        } catch (Exception e) {
+            // we don't need to delete directories since HMS will roll back db 
and fs if failed.
+            throw new RuntimeException("Failed to create table " + 
identifier.getFullName(), e);
+        }
+    }
+
+    @Override
+    public void dropView(Identifier identifier, boolean ignoreIfNotExists)
+            throws ViewNotExistException {
+        try {
+            getView(identifier);
+        } catch (ViewNotExistException e) {
+            if (ignoreIfNotExists) {
+                return;
+            }
+            throw e;
+        }
+
+        try {
+            clients.execute(
+                    client ->
+                            client.dropTable(
+                                    identifier.getDatabaseName(),
+                                    identifier.getTableName(),
+                                    false,
+                                    false,
+                                    false));
+        } catch (TException e) {
+            throw new RuntimeException("Failed to drop view " + 
identifier.getFullName(), e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException(
+                    "Interrupted in call to drop view " + 
identifier.getFullName(), e);
+        }
+    }
+
+    @Override
+    public List<String> listViews(String databaseName) throws 
DatabaseNotExistException {
+        if (isSystemDatabase(databaseName)) {
+            return Collections.emptyList();
+        }
+        if (!databaseExists(databaseName)) {
+            throw new DatabaseNotExistException(databaseName);
+        }
+
+        try {
+            List<String> tables = clients.run(client -> 
client.getAllTables(databaseName));
+            List<String> views = new ArrayList<>();
+            for (String tableName : tables) {
+                Table table = clients.run(client -> 
client.getTable(databaseName, tableName));
+                if (isView(table)) {
+                    views.add(tableName);
+                }
+            }
+            return views;
+        } catch (TException e) {
+            throw new RuntimeException("Failed to list all tables in database 
" + databaseName, e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted in call to listTables " + 
databaseName, e);
+        }
+    }
+
     @Override
     public FormatTable getFormatTable(Identifier identifier) throws 
TableNotExistException {
         if (!formatTableEnabled()) {
@@ -491,12 +627,13 @@ public class HiveCatalog extends AbstractCatalog {
             throw new RuntimeException(e);
         }
         try {
-            return HiveFormatTableUtils.convertToFormatTable(table);
+            return HiveTableUtils.convertToFormatTable(table);
         } catch (UnsupportedOperationException e) {
             throw new TableNotExistException(identifier);
         }
     }
 
+    @Override
     public void createFormatTable(Identifier identifier, Schema schema) {
         if (!formatTableEnabled()) {
             throw new UnsupportedOperationException(
@@ -1134,4 +1271,8 @@ public class HiveCatalog extends AbstractCatalog {
     public static String possibleHiveConfPath() {
         return System.getenv("HIVE_CONF_DIR");
     }
+
+    private static boolean isView(Table table) {
+        return TableType.valueOf(table.getTableType()) == 
TableType.VIRTUAL_VIEW;
+    }
 }
diff --git 
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveFormatTableUtils.java
 
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveTableUtils.java
similarity index 93%
rename from 
paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveFormatTableUtils.java
rename to 
paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveTableUtils.java
index dcd0c3c45..0c88107c0 100644
--- 
a/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveFormatTableUtils.java
+++ 
b/paimon-hive/paimon-hive-catalog/src/main/java/org/apache/paimon/hive/HiveTableUtils.java
@@ -44,7 +44,7 @@ import static org.apache.paimon.TableType.FORMAT_TABLE;
 import static org.apache.paimon.catalog.Catalog.COMMENT_PROP;
 import static org.apache.paimon.table.FormatTableOptions.FIELD_DELIMITER;
 
-class HiveFormatTableUtils {
+class HiveTableUtils {
 
     public static FormatTable convertToFormatTable(Table hiveTable) {
         if (TableType.valueOf(hiveTable.getTableType()) == 
TableType.VIRTUAL_VIEW) {
@@ -54,7 +54,7 @@ class HiveFormatTableUtils {
         Identifier identifier = new Identifier(hiveTable.getDbName(), 
hiveTable.getTableName());
         Map<String, String> options = new HashMap<>(hiveTable.getParameters());
         List<String> partitionKeys = 
getFieldNames(hiveTable.getPartitionKeys());
-        RowType rowType = createRowType(hiveTable.getSd().getCols(), 
hiveTable.getPartitionKeys());
+        RowType rowType = createRowType(hiveTable);
         String comment = options.remove(COMMENT_PROP);
         String location = hiveTable.getSd().getLocation();
         Format format;
@@ -104,10 +104,9 @@ class HiveFormatTableUtils {
     }
 
     /** Create a Paimon's Schema from Hive table's columns and partition keys. 
*/
-    private static RowType createRowType(
-            List<FieldSchema> nonPartCols, List<FieldSchema> partitionKeys) {
-        List<FieldSchema> allCols = new ArrayList<>(nonPartCols);
-        allCols.addAll(partitionKeys);
+    public static RowType createRowType(Table table) {
+        List<FieldSchema> allCols = new ArrayList<>(table.getSd().getCols());
+        allCols.addAll(table.getPartitionKeys());
         Pair<String[], DataType[]> columnInformation = 
extractColumnInformation(allCols);
         return RowType.builder()
                 .fields(columnInformation.getRight(), 
columnInformation.getLeft())
diff --git 
a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
 
b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
index 6b13a80e8..cb902605c 100644
--- 
a/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
+++ 
b/paimon-hive/paimon-hive-catalog/src/test/java/org/apache/paimon/hive/HiveCatalogTest.java
@@ -268,4 +268,9 @@ public class HiveCatalogTest extends CatalogTestBase {
             fail("Test failed due to exception: " + e.getMessage());
         }
     }
+
+    @Override
+    protected boolean supportsView() {
+        return true;
+    }
 }
diff --git 
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
 
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
index 246822600..cca044f04 100644
--- 
a/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
+++ 
b/paimon-hive/paimon-hive-connector-common/src/test/java/org/apache/paimon/hive/HiveCatalogITCaseBase.java
@@ -1911,6 +1911,33 @@ public abstract class HiveCatalogITCaseBase {
                 .containsExactlyInAnyOrder("dt=9998-06-15", "dt=9999-06-15");
     }
 
+    @Test
+    public void testView() throws Exception {
+        tEnv.executeSql("CREATE TABLE t ( a INT, b STRING ) WITH ( 
'file.format' = 'avro' )")
+                .await();
+        tEnv.executeSql("INSERT INTO t VALUES (1, 'Hi'), (2, 
'Hello')").await();
+
+        // test flink view
+        tEnv.executeSql("CREATE VIEW flink_v AS SELECT a + 1, b FROM 
t").await();
+        assertThat(collect("SELECT * FROM flink_v"))
+                .containsExactlyInAnyOrder(Row.of(2, "Hi"), Row.of(3, 
"Hello"));
+        assertThat(hiveShell.executeQuery("SELECT * FROM flink_v"))
+                .containsExactlyInAnyOrder("2\tHi", "3\tHello");
+
+        // test hive view
+        hiveShell.executeQuery("CREATE VIEW hive_v AS SELECT a + 1, b FROM t");
+        assertThat(collect("SELECT * FROM hive_v"))
+                .containsExactlyInAnyOrder(Row.of(2, "Hi"), Row.of(3, 
"Hello"));
+        assertThat(hiveShell.executeQuery("SELECT * FROM hive_v"))
+                .containsExactlyInAnyOrder("2\tHi", "3\tHello");
+
+        assertThat(collect("SHOW VIEWS"))
+                .containsExactlyInAnyOrder(Row.of("flink_v"), 
Row.of("hive_v"));
+
+        collect("DROP VIEW flink_v");
+        collect("DROP VIEW hive_v");
+    }
+
     /** Prepare to update a paimon table with a custom path in the paimon file 
system. */
     private void alterTableInFileSystem(TableEnvironment tEnv) throws 
Exception {
         tEnv.executeSql(
@@ -1974,14 +2001,4 @@ public abstract class HiveCatalogITCaseBase {
         }
         return result;
     }
-
-    private List<String> collectString(String sql) throws Exception {
-        List<String> result = new ArrayList<>();
-        try (CloseableIterator<Row> it = tEnv.executeSql(sql).collect()) {
-            while (it.hasNext()) {
-                result.add(it.next().toString());
-            }
-        }
-        return result;
-    }
 }


Reply via email to