This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 3a8cc60bc0 [#11187] feat(flink-connector): Add view support (#11189)
3a8cc60bc0 is described below
commit 3a8cc60bc02f3f1c625454a99edcb593fafefb1f
Author: Yuhui <[email protected]>
AuthorDate: Wed May 27 10:59:32 2026 +0800
[#11187] feat(flink-connector): Add view support (#11189)
### What changes were proposed in this pull request?
Route Flink SQL view operations (CREATE/DROP/ALTER/LIST VIEW) through
Gravitino's `ViewCatalog` API in `BaseCatalog`. Any Gravitino catalog
that implements `ViewCatalog` gets view support automatically.
### Why are the changes needed?
Fix: #11187
### Does this PR introduce _any_ user-facing change?
Yes — users can now use `CREATE VIEW`, `DROP VIEW`, `ALTER VIEW`,
`SHOW VIEWS`, and `SELECT` from views via Flink SQL on Gravitino
catalogs.
### How was this patch tested?
- Unit tests: `TestBaseCatalog` — listViews, view change conversion
- Integration tests: 9 new cases in `FlinkHiveCatalogIT` covering
create, list, drop, rename, replace body, query, and idempotency
---
.../catalog/hive/HiveCatalogOperations.java | 4 +-
.../apache/gravitino/catalog/hive/HiveView.java | 9 +-
.../catalog/hive/HiveViewCatalogOperations.java | 52 +--
.../catalog/hive/TestHiveCatalogOperations.java | 8 +-
.../flink/connector/catalog/BaseCatalog.java | 354 ++++++++++++++++++---
.../flink/connector/hive/GravitinoHiveCatalog.java | 22 +-
.../flink/connector/catalog/TestBaseCatalog.java | 160 +++++++++-
.../integration/test/hive/FlinkHiveCatalogIT.java | 275 ++++++++++++++++
8 files changed, 804 insertions(+), 80 deletions(-)
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
index 5ff4f27ddc..87825ca913 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveCatalogOperations.java
@@ -476,7 +476,9 @@ public class HiveCatalogOperations
@Override
public Table loadTable(NameIdentifier tableIdent) throws
NoSuchTableException {
HiveTableHandle hiveTable = loadHiveTable(tableIdent);
-
+ if
(TableType.VIRTUAL_VIEW.name().equalsIgnoreCase(hiveTable.getTableType())) {
+ throw new NoSuchTableException("Table %s is a view, not a table",
tableIdent);
+ }
LOG.info("Loaded Hive table {} from Hive Metastore ", tableIdent.name());
return hiveTable;
}
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveView.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveView.java
index 1e28045c55..bae828e7f6 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveView.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveView.java
@@ -41,8 +41,8 @@ import org.apache.gravitino.rel.View;
/**
* Represents a view stored in Hive Metastore (VIRTUAL_VIEW table type). The
SQL dialect is detected
* from table properties: Trino views start with "/* Presto View:", Spark
views carry {@code
- * spark.sql.create.version} in their parameters, and all other views are
treated as native Hive SQL
- * views.
+ * spark.sql.create.version}, Flink views carry properties prefixed with
{@code flink.}, and all
+ * other views are treated as native Hive SQL views.
*/
@Unstable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -112,7 +112,7 @@ public class HiveView implements View {
*
* @param viewOriginalText The original view text from HMS.
* @param parameters The HMS table parameters map.
- * @return The detected dialect string: "trino", "spark", or "hive".
+ * @return The detected dialect string: "trino", "spark", "flink", or "hive".
*/
static String detectDialect(String viewOriginalText, Map<String, String>
parameters) {
if (parameters != null &&
"true".equalsIgnoreCase(parameters.get(TRINO_VIEW_MARKER_KEY))) {
@@ -124,6 +124,9 @@ public class HiveView implements View {
if (parameters != null && parameters.containsKey(SPARK_VERSION_KEY)) {
return Dialects.SPARK;
}
+ if (parameters != null && parameters.keySet().stream().anyMatch(k ->
k.startsWith("flink."))) {
+ return Dialects.FLINK;
+ }
return Dialects.HIVE;
}
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveViewCatalogOperations.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveViewCatalogOperations.java
index e35b265949..1200391bbc 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveViewCatalogOperations.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveViewCatalogOperations.java
@@ -363,18 +363,19 @@ class HiveViewCatalogOperations implements ViewCatalog {
Maps.newHashMap(properties != null ? properties : ImmutableMap.of());
String representationSql = viewOriginalText;
String detectedDialect = HiveView.detectDialect(representationSql, params);
- if (!Dialects.HIVE.equalsIgnoreCase(detectedDialect)) {
+ if (!Dialects.HIVE.equalsIgnoreCase(detectedDialect)
+ && !Dialects.FLINK.equalsIgnoreCase(detectedDialect)) {
// TODO(design-docs/gravitino-logical-view-management.md): support
loading trino/spark HMS
// views.
throw new UnsupportedOperationException(
String.format(
- "Hive catalog currently supports only '%s' view dialect, but
found '%s' for view %s",
- Dialects.HIVE, detectedDialect, ident));
+ "Hive catalog currently supports only '%s' and '%s' view
dialects, but found '%s' for view %s",
+ Dialects.HIVE, Dialects.FLINK, detectedDialect, ident));
}
SQLRepresentation rep =
SQLRepresentation.builder()
- .withDialect(Dialects.HIVE)
+ .withDialect(detectedDialect)
.withSql(StringUtils.defaultString(representationSql))
.build();
@@ -405,34 +406,37 @@ class HiveViewCatalogOperations implements ViewCatalog {
firstRepresentation == null ? "null" :
firstRepresentation.getClass().getSimpleName());
SQLRepresentation selected = (SQLRepresentation) firstRepresentation;
- boolean isHiveDialect = Dialects.HIVE.equalsIgnoreCase(selected.dialect());
- if (isHiveDialect) {
- Preconditions.checkArgument(
- defaultCatalog == null && defaultSchema == null,
- "Hive dialect '%s' does not support non-null
defaultCatalog/defaultSchema, but got "
- + "defaultCatalog=%s, defaultSchema=%s for view %s",
- Dialects.HIVE,
- defaultCatalog,
- defaultSchema,
- ident);
- return selected;
+ switch (selected.dialect().toLowerCase(java.util.Locale.ROOT)) {
+ case Dialects.HIVE:
+ case Dialects.FLINK:
+ Preconditions.checkArgument(
+ defaultCatalog == null && defaultSchema == null,
+ "Dialect '%s' does not support non-null
defaultCatalog/defaultSchema, but got "
+ + "defaultCatalog=%s, defaultSchema=%s for view %s",
+ selected.dialect(),
+ defaultCatalog,
+ defaultSchema,
+ ident);
+ return selected;
+ default:
+ // TODO(design-docs/gravitino-logical-view-management.md): support
creating trino/spark HMS
+ // views.
+ throw new UnsupportedOperationException(
+ String.format(
+ "Hive catalog currently supports only '%s' and '%s' view
dialects, but got '%s' for view %s",
+ Dialects.HIVE, Dialects.FLINK, selected.dialect(), ident));
}
- // TODO(design-docs/gravitino-logical-view-management.md): support
creating trino/spark HMS
- // views.
- throw new UnsupportedOperationException(
- String.format(
- "Hive catalog currently supports only '%s' view dialect, but got
'%s' for view %s",
- Dialects.HIVE, selected.dialect(), ident));
}
private String toHmsViewOriginalText(SQLRepresentation representation,
NameIdentifier ident) {
- if (!Dialects.HIVE.equalsIgnoreCase(representation.dialect())) {
+ if (!Dialects.HIVE.equalsIgnoreCase(representation.dialect())
+ && !Dialects.FLINK.equalsIgnoreCase(representation.dialect())) {
// TODO(design-docs/gravitino-logical-view-management.md): support
serializing trino/spark HMS
// view definitions.
throw new UnsupportedOperationException(
String.format(
- "Hive catalog currently supports only '%s' view dialect, but got
'%s' for view %s",
- Dialects.HIVE, representation.dialect(), ident));
+ "Hive catalog currently supports only '%s' and '%s' view
dialects, but got '%s' for view %s",
+ Dialects.HIVE, Dialects.FLINK, representation.dialect(), ident));
}
return representation.sql();
}
diff --git
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
index 56154cbf41..a8ab3142d9 100644
---
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
+++
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestHiveCatalogOperations.java
@@ -245,7 +245,7 @@ class TestHiveCatalogOperations {
null,
null,
Maps.newHashMap()));
- Assertions.assertTrue(exception.getMessage().contains("supports only
'hive'"));
+ Assertions.assertTrue(exception.getMessage().contains("supports only"));
}
@Test
@@ -403,7 +403,7 @@ class TestHiveCatalogOperations {
null,
null,
Maps.newHashMap()));
- Assertions.assertTrue(exception.getMessage().contains("supports only
'hive'"));
+ Assertions.assertTrue(exception.getMessage().contains("supports only"));
}
@Test
@@ -510,7 +510,7 @@ class TestHiveCatalogOperations {
Assertions.assertThrows(
UnsupportedOperationException.class,
() -> op.loadView(NameIdentifier.of("db", "v_trino")));
- Assertions.assertTrue(exception.getMessage().contains("supports only
'hive'"));
+ Assertions.assertTrue(exception.getMessage().contains("supports only"));
}
@Test
@@ -628,7 +628,7 @@ class TestHiveCatalogOperations {
null,
null,
null)));
- Assertions.assertTrue(exception.getMessage().contains("supports only
'hive'"));
+ Assertions.assertTrue(exception.getMessage().contains("supports only"));
}
@Test
diff --git
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
index 3fdbbb4eef..f3fa79c29d 100644
---
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
+++
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/catalog/BaseCatalog.java
@@ -41,8 +41,10 @@ import org.apache.flink.table.catalog.CatalogFunction;
import org.apache.flink.table.catalog.CatalogPartition;
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.ObjectPath;
import org.apache.flink.table.catalog.ResolvedCatalogBaseTable;
+import org.apache.flink.table.catalog.ResolvedCatalogView;
import org.apache.flink.table.catalog.UniqueConstraint;
import org.apache.flink.table.catalog.exceptions.CatalogException;
import org.apache.flink.table.catalog.exceptions.DatabaseAlreadyExistException;
@@ -69,9 +71,11 @@ import org.apache.gravitino.SchemaChange;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
import org.apache.gravitino.exceptions.TableAlreadyExistsException;
+import org.apache.gravitino.exceptions.ViewAlreadyExistsException;
import org.apache.gravitino.flink.connector.PartitionConverter;
import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
import org.apache.gravitino.flink.connector.utils.CatalogCompat;
@@ -79,20 +83,31 @@ import
org.apache.gravitino.flink.connector.utils.DefaultCatalogCompat;
import org.apache.gravitino.flink.connector.utils.TableUtils;
import org.apache.gravitino.flink.connector.utils.TypeUtils;
import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Dialects;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.SQLRepresentation;
import org.apache.gravitino.rel.Table;
import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.rel.ViewChange;
import org.apache.gravitino.rel.expressions.distributions.Distribution;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.rel.indexes.Indexes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* The BaseCatalog that provides a default implementation for all methods in
the {@link
* org.apache.flink.table.catalog.Catalog} interface.
*/
public abstract class BaseCatalog extends AbstractCatalog {
+ private static final Logger LOG = LoggerFactory.getLogger(BaseCatalog.class);
+ private static final String FLINK_SCHEMA_NUM_COLUMNS_KEY =
"flink.schema.num-columns";
+
private final SchemaAndTablePropertiesConverter
schemaAndTablePropertiesConverter;
private final PartitionConverter partitionConverter;
private final Map<String, String> catalogOptions;
@@ -210,32 +225,71 @@ public abstract class BaseCatalog extends AbstractCatalog
{
@Override
public List<String> listViews(String databaseName)
throws DatabaseNotExistException, CatalogException {
- // Gravitino does not support views yet; return empty to keep Flink
callers happy.
- return Collections.emptyList();
+ try {
+ ViewCatalog viewCatalog = catalog().asViewCatalog();
+ // TODO: Currently returns all VIRTUAL_VIEW entries from the underlying
catalog regardless of
+ // dialect. Views created by other engines (e.g. Trino, Spark) may
appear here but will fail
+ // when Flink attempts to load them. Consider filtering to only dialects
that Flink can handle
+ // (hive, flink), but this requires per-view property inspection which
is expensive.
+ return Arrays.stream(viewCatalog.listViews(Namespace.of(databaseName)))
+ .map(NameIdentifier::name)
+ .collect(Collectors.toList());
+ } catch (UnsupportedOperationException e) {
+ // Flink's listViews contract allows returning an empty list when views
are not supported.
+ LOG.debug("Catalog {} does not support views; returning empty view
list", catalogName(), e);
+ return Collections.emptyList();
+ } catch (NoSuchSchemaException e) {
+ throw new DatabaseNotExistException(catalogName(), databaseName, e);
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
}
@Override
public CatalogBaseTable getTable(ObjectPath tablePath)
throws TableNotExistException, CatalogException {
+ NameIdentifier ident =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
try {
- Table table =
- catalog()
- .asTableCatalog()
- .loadTable(NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName()));
+ Table table = catalog().asTableCatalog().loadTable(ident);
return toFlinkTable(table, tablePath);
} catch (NoSuchTableException e) {
- throw new TableNotExistException(catalogName(), tablePath, e);
+ // Fall through to check views.
} catch (Exception e) {
+ LOG.warn("Failed to load table {} from catalog {}", ident,
catalogName(), e);
throw new CatalogException(e);
}
+
+ return loadViewOrThrow(tablePath);
}
@Override
public boolean tableExists(ObjectPath tablePath) throws CatalogException {
+ NameIdentifier ident =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
try {
- return catalog()
- .asTableCatalog()
- .tableExists(NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName()));
+ if (catalog().asTableCatalog().tableExists(ident)) {
+ return true;
+ }
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
+ try {
+ return catalog().asViewCatalog().viewExists(ident);
+ } catch (UnsupportedOperationException e) {
+ LOG.debug(
+ "Catalog {} does not support views; viewExists returns false for {}",
+ catalogName(),
+ ident,
+ e);
+ return false;
+ } catch (NoSuchSchemaException e) {
+ LOG.debug(
+ "Schema not found when checking viewExists for {} in catalog {};
returning false",
+ ident,
+ catalogName(),
+ e);
+ return false;
} catch (Exception e) {
throw new CatalogException(e);
}
@@ -244,36 +298,64 @@ public abstract class BaseCatalog extends AbstractCatalog
{
@Override
public void dropTable(ObjectPath tablePath, boolean ignoreIfNotExists)
throws TableNotExistException, CatalogException {
- boolean dropped =
- catalog()
- .asTableCatalog()
- .dropTable(NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName()));
- if (!dropped && !ignoreIfNotExists) {
- throw new TableNotExistException(catalogName(), tablePath);
+ NameIdentifier ident =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
+
+ try {
+ boolean tableDropped = catalog().asTableCatalog().dropTable(ident);
+ boolean viewDropped = false;
+ try {
+ viewDropped = catalog().asViewCatalog().dropView(ident);
+ } catch (UnsupportedOperationException ignored) {
+ // catalog does not support views
+ }
+ if (!tableDropped && !viewDropped && !ignoreIfNotExists) {
+ throw new TableNotExistException(catalogName(), tablePath);
+ }
+ } catch (TableNotExistException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new CatalogException(e);
}
}
@Override
public void renameTable(ObjectPath tablePath, String newTableName, boolean
ignoreIfNotExists)
throws TableNotExistException, TableAlreadyExistException,
CatalogException {
- NameIdentifier identifier =
- NameIdentifier.of(Namespace.of(tablePath.getDatabaseName()),
newTableName);
+ NameIdentifier srcIdent =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
+ ObjectPath newPath = new ObjectPath(tablePath.getDatabaseName(),
newTableName);
+
+ try {
+ catalog().asTableCatalog().alterTable(srcIdent,
TableChange.rename(newTableName));
+ return;
+ } catch (NoSuchTableException ignored) {
+ // source is not a table, try as a view below
+ } catch (TableAlreadyExistsException e) {
+ throw new TableAlreadyExistException(catalogName(), newPath, e);
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
- if (catalog().asTableCatalog().tableExists(identifier)) {
- throw new TableAlreadyExistException(
- catalogName(), ObjectPath.fromString(tablePath.getDatabaseName() +
newTableName));
+ ViewCatalog viewCatalog;
+ try {
+ viewCatalog = catalog().asViewCatalog();
+ } catch (UnsupportedOperationException e) {
+ // catalog does not support views; source does not exist as table or view
+ if (!ignoreIfNotExists) {
+ throw new TableNotExistException(catalogName(), tablePath);
+ }
+ return;
}
try {
- catalog()
- .asTableCatalog()
- .alterTable(
- NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName()),
- TableChange.rename(newTableName));
- } catch (NoSuchTableException e) {
+ viewCatalog.alterView(srcIdent, ViewChange.rename(newTableName));
+ } catch (NoSuchViewException e) {
if (!ignoreIfNotExists) {
throw new TableNotExistException(catalogName(), tablePath, e);
}
+ } catch (ViewAlreadyExistsException e) {
+ throw new TableAlreadyExistException(catalogName(), newPath, e);
} catch (Exception e) {
throw new CatalogException(e);
}
@@ -284,13 +366,19 @@ public abstract class BaseCatalog extends AbstractCatalog
{
throws TableAlreadyExistException, DatabaseNotExistException,
CatalogException {
Preconditions.checkArgument(
table instanceof ResolvedCatalogBaseTable, "table should be resolved");
+
+ if (table.getTableKind() == CatalogBaseTable.TableKind.VIEW) {
+ createView(tablePath, (ResolvedCatalogView) table, ignoreIfExists);
+ return;
+ }
+
NameIdentifier identifier =
NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
ResolvedCatalogBaseTable<?> resolvedTable = (ResolvedCatalogBaseTable<?>)
table;
Column[] columns =
resolvedTable.getResolvedSchema().getColumns().stream()
- .map(this::toGravitinoColumn)
+ .map(BaseCatalog::toGravitinoColumn)
.toArray(Column[]::new);
String comment = table.getComment();
Map<String, String> flinkOptions = table.getOptions();
@@ -323,6 +411,30 @@ public abstract class BaseCatalog extends AbstractCatalog {
}
}
+ private void createView(ObjectPath tablePath, ResolvedCatalogView view,
boolean ignoreIfExists)
+ throws TableAlreadyExistException, DatabaseNotExistException,
CatalogException {
+ NameIdentifier identifier =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
+ Column[] columns = toGravitinoColumns(view);
+ Representation[] representations =
+ buildSqlRepresentation(Dialects.FLINK, view.getExpandedQuery());
+ Map<String, String> options = new HashMap<>(view.getOptions());
+ options.put(FLINK_SCHEMA_NUM_COLUMNS_KEY, String.valueOf(columns.length));
+ try {
+ catalog()
+ .asViewCatalog()
+ .createView(identifier, view.getComment(), columns, representations,
null, null, options);
+ } catch (NoSuchSchemaException e) {
+ throw new DatabaseNotExistException(catalogName(),
tablePath.getDatabaseName(), e);
+ } catch (ViewAlreadyExistsException e) {
+ if (!ignoreIfExists) {
+ throw new TableAlreadyExistException(catalogName(), tablePath, e);
+ }
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
+ }
+
private static Index[] getGrivatinoIndices(ResolvedCatalogBaseTable<?>
resolvedTable) {
Optional<UniqueConstraint> primaryKey =
resolvedTable.getResolvedSchema().getPrimaryKey();
List<String> primaryColumns =
primaryKey.map(UniqueConstraint::getColumns).orElse(null);
@@ -371,9 +483,26 @@ public abstract class BaseCatalog extends AbstractCatalog {
NameIdentifier identifier =
NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
- catalog()
- .asTableCatalog()
- .alterTable(identifier, getGravitinoTableChanges(existingTable,
newTable));
+
+ if (newTable.getTableKind() == CatalogBaseTable.TableKind.VIEW) {
+ try {
+ catalog()
+ .asViewCatalog()
+ .alterView(
+ identifier,
+ toReplaceViewChange(existingTable, (ResolvedCatalogView)
newTable, Dialects.FLINK));
+ } catch (NoSuchViewException e) {
+ if (!ignoreIfNotExists) {
+ throw new TableNotExistException(catalogName(), tablePath, e);
+ }
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
+ } else {
+ catalog()
+ .asTableCatalog()
+ .alterTable(identifier, getGravitinoTableChanges(existingTable,
newTable));
+ }
}
@Override
@@ -402,7 +531,24 @@ public abstract class BaseCatalog extends AbstractCatalog {
NameIdentifier identifier =
NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
- catalog().asTableCatalog().alterTable(identifier,
getGravitinoTableChanges(tableChanges));
+
+ if (newTable.getTableKind() == CatalogBaseTable.TableKind.VIEW) {
+ try {
+ catalog()
+ .asViewCatalog()
+ .alterView(
+ identifier,
+ toReplaceViewChange(tableChanges, (ResolvedCatalogView)
newTable, Dialects.FLINK));
+ } catch (NoSuchViewException e) {
+ if (!ignoreIfNotExists) {
+ throw new TableNotExistException(catalogName(), tablePath, e);
+ }
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
+ } else {
+ catalog().asTableCatalog().alterTable(identifier,
getGravitinoTableChanges(tableChanges));
+ }
}
@Override
@@ -561,14 +707,7 @@ public abstract class BaseCatalog extends AbstractCatalog {
}
protected CatalogBaseTable toFlinkTable(Table table, ObjectPath tablePath) {
- org.apache.flink.table.api.Schema.Builder builder =
- org.apache.flink.table.api.Schema.newBuilder();
- for (Column column : table.columns()) {
- DataType flinkType = TypeUtils.toFlinkType(column.dataType());
- builder
- .column(column.name(), column.nullable() ? flinkType.nullable() :
flinkType.notNull())
- .withComment(column.comment());
- }
+ org.apache.flink.table.api.Schema.Builder builder =
buildSchemaFromColumns(table.columns());
Optional<List<String>> flinkPrimaryKey = getFlinkPrimaryKey(table);
flinkPrimaryKey.ifPresent(builder::primaryKey);
Map<String, String> flinkTableProperties =
@@ -618,7 +757,7 @@ public abstract class BaseCatalog extends AbstractCatalog {
return Optional.of(primaryKeyFieldList);
}
- private Column toGravitinoColumn(org.apache.flink.table.catalog.Column
column) {
+ private static Column
toGravitinoColumn(org.apache.flink.table.catalog.Column column) {
return Column.of(
column.getName(),
TypeUtils.toGravitinoType(column.getDataType().getLogicalType()),
@@ -748,6 +887,139 @@ public abstract class BaseCatalog extends AbstractCatalog
{
return schemaChanges.toArray(new SchemaChange[0]);
}
+ /**
+ * Loads the entity at {@code tablePath} as a Flink {@link CatalogView} if
it is a view in the
+ * underlying ViewCatalog. Throws {@link TableNotExistException} if no view
is found (or if the
+ * catalog does not support views).
+ */
+ protected CatalogBaseTable loadViewOrThrow(ObjectPath tablePath) throws
TableNotExistException {
+ NameIdentifier ident =
+ NameIdentifier.of(tablePath.getDatabaseName(),
tablePath.getObjectName());
+ try {
+ View view = catalog().asViewCatalog().loadView(ident);
+ return toFlinkView(view);
+ } catch (NoSuchViewException e) {
+ throw new TableNotExistException(catalogName(), tablePath, e);
+ } catch (UnsupportedOperationException e) {
+ LOG.debug(
+ "Catalog {} does not support views; treating {} as not found",
+ catalogName(),
+ tablePath,
+ e);
+ throw new TableNotExistException(catalogName(), tablePath, e);
+ } catch (Exception e) {
+ throw new CatalogException(e);
+ }
+ }
+
+ /**
+ * Converts a Gravitino {@link View} to a Flink {@link CatalogView}.
+ *
+ * @param view The Gravitino view to convert.
+ * @return The corresponding Flink CatalogView.
+ */
+ protected CatalogView toFlinkView(View view) {
+ org.apache.flink.table.api.Schema.Builder builder =
buildSchemaFromColumns(view.columns());
+ String sql =
+ view.sqlFor(Dialects.FLINK)
+ .map(SQLRepresentation::sql)
+ .orElseGet(
+ () ->
+ view.sqlFor(Dialects.HIVE)
+ .map(SQLRepresentation::sql)
+ .orElseThrow(
+ () ->
+ new CatalogException(
+ String.format(
+ "View '%s' in catalog '%s' has no SQL
representation for dialect '%s' or '%s'",
+ view.name(),
+ catalogName(),
+ Dialects.FLINK,
+ Dialects.HIVE))));
+
+ Map<String, String> properties =
+ view.properties() != null
+ ? Collections.unmodifiableMap(view.properties())
+ : Collections.emptyMap();
+ return CatalogView.of(builder.build(), view.comment(), sql, sql,
properties);
+ }
+
+ @VisibleForTesting
+ static ViewChange[] toReplaceViewChange(
+ CatalogBaseTable existingView, ResolvedCatalogView newView, String
dialect) {
+ return new ViewChange[] {
+ ViewChange.replaceView(
+ toGravitinoColumns(newView),
+ buildSqlRepresentation(dialect, newView.getExpandedQuery()),
+ null,
+ null,
+ newView.getComment())
+ };
+ }
+
+ @VisibleForTesting
+ static ViewChange[] toReplaceViewChange(
+ List<org.apache.flink.table.catalog.TableChange> tableChanges,
+ ResolvedCatalogView newView,
+ String dialect) {
+ List<ViewChange> changes = Lists.newArrayList();
+ boolean needsBodyReplace = false;
+
+ for (org.apache.flink.table.catalog.TableChange change : tableChanges) {
+ if (change instanceof
org.apache.flink.table.catalog.TableChange.SetOption) {
+ org.apache.flink.table.catalog.TableChange.SetOption setOption =
+ (org.apache.flink.table.catalog.TableChange.SetOption) change;
+ changes.add(ViewChange.setProperty(setOption.getKey(),
setOption.getValue()));
+ } else if (change instanceof
org.apache.flink.table.catalog.TableChange.ResetOption) {
+ org.apache.flink.table.catalog.TableChange.ResetOption resetOption =
+ (org.apache.flink.table.catalog.TableChange.ResetOption) change;
+ changes.add(ViewChange.removeProperty(resetOption.getKey()));
+ } else {
+ LOG.warn(
+ "Unrecognized TableChange type {} for view alter; triggering full
ReplaceView",
+ change.getClass().getSimpleName());
+ needsBodyReplace = true;
+ }
+ }
+
+ if (needsBodyReplace) {
+ changes.add(
+ ViewChange.replaceView(
+ toGravitinoColumns(newView),
+ buildSqlRepresentation(dialect, newView.getExpandedQuery()),
+ null,
+ null,
+ newView.getComment()));
+ }
+
+ return changes.toArray(new ViewChange[0]);
+ }
+
+ private static Column[] toGravitinoColumns(ResolvedCatalogView view) {
+ return view.getResolvedSchema().getColumns().stream()
+ .map(BaseCatalog::toGravitinoColumn)
+ .toArray(Column[]::new);
+ }
+
+ private static org.apache.flink.table.api.Schema.Builder
buildSchemaFromColumns(
+ Column[] columns) {
+ org.apache.flink.table.api.Schema.Builder builder =
+ org.apache.flink.table.api.Schema.newBuilder();
+ for (Column column : columns) {
+ DataType flinkType = TypeUtils.toFlinkType(column.dataType());
+ builder
+ .column(column.name(), column.nullable() ? flinkType.nullable() :
flinkType.notNull())
+ .withComment(column.comment());
+ }
+ return builder;
+ }
+
+ private static Representation[] buildSqlRepresentation(String dialect,
String sql) {
+ return new Representation[] {
+ SQLRepresentation.builder().withDialect(dialect).withSql(sql).build()
+ };
+ }
+
protected Catalog catalog() {
return GravitinoCatalogManager.get().getGravitinoCatalogInfo(getName());
}
diff --git
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
index 0a32ce4373..a027b36dd4 100644
---
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
+++
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/hive/GravitinoHiveCatalog.java
@@ -30,6 +30,7 @@ import org.apache.flink.table.catalog.AbstractCatalog;
import org.apache.flink.table.catalog.CatalogBaseTable;
import org.apache.flink.table.catalog.CatalogPropertiesUtil;
import org.apache.flink.table.catalog.CatalogTable;
+import org.apache.flink.table.catalog.CatalogView;
import org.apache.flink.table.catalog.ObjectPath;
import org.apache.flink.table.catalog.ResolvedCatalogBaseTable;
import org.apache.flink.table.catalog.ResolvedCatalogTable;
@@ -54,12 +55,15 @@ import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
import org.apache.hadoop.hive.conf.HiveConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* The GravitinoHiveCatalog class is an implementation of the BaseCatalog
class that is used to
* proxy the HiveCatalog class.
*/
public class GravitinoHiveCatalog extends BaseCatalog {
+ private static final Logger LOG =
LoggerFactory.getLogger(GravitinoHiveCatalog.class);
private HiveCatalog hiveCatalog;
@@ -100,6 +104,11 @@ public class GravitinoHiveCatalog extends BaseCatalog {
Preconditions.checkArgument(
table instanceof ResolvedCatalogBaseTable, "table should be resolved");
+ if (table instanceof CatalogView) {
+ super.createTable(tablePath, table, ignoreIfExists);
+ return;
+ }
+
if (!FlinkGenericTableUtil.isGenericTableWhenCreate(table.getOptions())) {
super.createTable(tablePath, table, ignoreIfExists);
return;
@@ -150,15 +159,22 @@ public class GravitinoHiveCatalog extends BaseCatalog {
}
return super.toFlinkTable(table, tablePath);
} catch (NoSuchTableException e) {
- throw new TableNotExistException(catalogName(), tablePath, e);
+ // Fall through to check views.
} catch (Exception e) {
+ LOG.warn("Failed to load table {} from catalog {}", tablePath,
catalogName(), e);
throw new CatalogException(e);
}
+
+ return loadViewOrThrow(tablePath);
}
@Override
public void alterTable(ObjectPath tablePath, CatalogBaseTable newTable,
boolean ignoreIfNotExists)
throws TableNotExistException, CatalogException {
+ if (newTable instanceof CatalogView) {
+ super.alterTable(tablePath, newTable, ignoreIfNotExists);
+ return;
+ }
Table table = loadGravitinoTable(tablePath, ignoreIfNotExists);
if (table == null) {
return;
@@ -183,6 +199,10 @@ public class GravitinoHiveCatalog extends BaseCatalog {
java.util.List<org.apache.flink.table.catalog.TableChange> tableChanges,
boolean ignoreIfNotExists)
throws TableNotExistException, CatalogException {
+ if (newTable instanceof CatalogView) {
+ super.alterTable(tablePath, newTable, tableChanges, ignoreIfNotExists);
+ return;
+ }
Table table = loadGravitinoTable(tablePath, ignoreIfNotExists);
if (table == null) {
return;
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
index ec934881df..34b7882fea 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/catalog/TestBaseCatalog.java
@@ -20,21 +20,34 @@ package org.apache.gravitino.flink.connector.catalog;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
import org.apache.flink.table.api.DataTypes;
import org.apache.flink.table.api.Schema;
import org.apache.flink.table.catalog.AbstractCatalog;
import org.apache.flink.table.catalog.CatalogBaseTable;
import org.apache.flink.table.catalog.CatalogDatabase;
import org.apache.flink.table.catalog.CatalogDatabaseImpl;
+import org.apache.flink.table.catalog.CatalogView;
import org.apache.flink.table.catalog.Column;
+import org.apache.flink.table.catalog.ResolvedCatalogView;
+import org.apache.flink.table.catalog.ResolvedSchema;
import org.apache.flink.table.catalog.TableChange;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
import org.apache.gravitino.SchemaChange;
import org.apache.gravitino.flink.connector.PartitionConverter;
import org.apache.gravitino.flink.connector.SchemaAndTablePropertiesConverter;
import org.apache.gravitino.flink.connector.utils.DefaultCatalogCompat;
+import org.apache.gravitino.rel.Dialects;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.SQLRepresentation;
+import org.apache.gravitino.rel.ViewCatalog;
+import org.apache.gravitino.rel.ViewChange;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
import org.apache.gravitino.rel.types.Types;
import org.junit.jupiter.api.Assertions;
@@ -125,7 +138,8 @@ public class TestBaseCatalog {
@Test
public void testToGravitinoDistributionDefaultsToNone() {
- TestableBaseCatalog catalog = new
TestableBaseCatalog(Mockito.mock(AbstractCatalog.class));
+ TestableBaseCatalog catalog =
+ new TestableBaseCatalog(Mockito.mock(AbstractCatalog.class),
mockUnsupportedViewCatalog());
Assertions.assertEquals(
Distributions.NONE,
@@ -136,21 +150,149 @@ public class TestBaseCatalog {
}
@Test
- public void testListViewsReturnsEmptyWithoutDelegation() throws Exception {
- AbstractCatalog delegate = Mockito.mock(AbstractCatalog.class);
- BaseCatalog catalog = new TestableBaseCatalog(delegate);
+ public void testListViewsReturnsEmptyWhenViewCatalogUnsupported() throws
Exception {
+ Catalog gravitinoCatalog = mockUnsupportedViewCatalog();
+ BaseCatalog catalog =
+ new TestableBaseCatalog(Mockito.mock(AbstractCatalog.class),
gravitinoCatalog);
List<String> views = catalog.listViews("db");
Assertions.assertTrue(views.isEmpty());
- Mockito.verifyNoInteractions(delegate);
+ }
+
+ @Test
+ public void testListViewsDelegatesToViewCatalog() throws Exception {
+ ViewCatalog viewCatalog = Mockito.mock(ViewCatalog.class);
+ Mockito.when(viewCatalog.listViews(Namespace.of("db")))
+ .thenReturn(
+ new NameIdentifier[] {NameIdentifier.of("db", "v1"),
NameIdentifier.of("db", "v2")});
+ Catalog gravitinoCatalog = Mockito.mock(Catalog.class);
+ Mockito.when(gravitinoCatalog.asViewCatalog()).thenReturn(viewCatalog);
+
+ BaseCatalog catalog =
+ new TestableBaseCatalog(Mockito.mock(AbstractCatalog.class),
gravitinoCatalog);
+
+ List<String> views = catalog.listViews("db");
+
+ Assertions.assertEquals(ImmutableList.of("v1", "v2"), views);
+ }
+
+ @Test
+ public void testGetGravitinoViewChangesSetAndRemoveProperty() {
+ List<TableChange> tableChanges =
+ ImmutableList.of(TableChange.set("k1", "v1"), TableChange.reset("k2"));
+
+ Schema schema = Schema.newBuilder().column("id", DataTypes.INT()).build();
+
+ ViewChange[] changes =
+ BaseCatalog.toReplaceViewChange(
+ tableChanges, resolveView(schema, "SELECT 1", "comment"),
Dialects.FLINK);
+
+ Assertions.assertEquals(2, changes.length);
+ Assertions.assertInstanceOf(ViewChange.SetProperty.class, changes[0]);
+ Assertions.assertEquals("k1", ((ViewChange.SetProperty)
changes[0]).getProperty());
+ Assertions.assertEquals("v1", ((ViewChange.SetProperty)
changes[0]).getValue());
+
+ Assertions.assertInstanceOf(ViewChange.RemoveProperty.class, changes[1]);
+ Assertions.assertEquals("k2", ((ViewChange.RemoveProperty)
changes[1]).getProperty());
+ }
+
+ @Test
+ public void testGetGravitinoViewChangesBodyReplaceOnStructuralChange() {
+ List<TableChange> tableChanges =
+ ImmutableList.of(
+ TableChange.add(Column.physical("id", DataTypes.INT())),
TableChange.set("k1", "v1"));
+
+ Schema schema = Schema.newBuilder().column("id", DataTypes.INT()).build();
+
+ ViewChange[] changes =
+ BaseCatalog.toReplaceViewChange(
+ tableChanges, resolveView(schema, "SELECT id FROM t", "new
comment"), Dialects.FLINK);
+
+ // Should have exactly one SetProperty and one ReplaceView (order may vary)
+ Assertions.assertEquals(2, changes.length);
+ ViewChange.SetProperty setProp =
+ (ViewChange.SetProperty)
+ Arrays.stream(changes)
+ .filter(c -> c instanceof ViewChange.SetProperty)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("expected SetProperty"));
+ Assertions.assertEquals("k1", setProp.getProperty());
+ Assertions.assertEquals("v1", setProp.getValue());
+
+ ViewChange.ReplaceView replaceView =
+ (ViewChange.ReplaceView)
+ Arrays.stream(changes)
+ .filter(c -> c instanceof ViewChange.ReplaceView)
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("expected ReplaceView"));
+ Assertions.assertEquals("new comment", replaceView.getComment());
+ Assertions.assertEquals(1, replaceView.getRepresentations().length);
+ Assertions.assertInstanceOf(SQLRepresentation.class,
replaceView.getRepresentations()[0]);
+ SQLRepresentation sqlRep = (SQLRepresentation)
replaceView.getRepresentations()[0];
+ Assertions.assertEquals(Dialects.FLINK, sqlRep.dialect());
+ Assertions.assertEquals("SELECT id FROM t", sqlRep.sql());
+ }
+
+ @Test
+ public void testGetGravitinoViewChangesFullReplace() {
+ Schema schema = Schema.newBuilder().column("id", DataTypes.INT()).build();
+ CatalogView existing =
+ CatalogView.of(schema, "old comment", "SELECT 1", "SELECT 1",
Collections.emptyMap());
+
+ ViewChange[] changes =
+ BaseCatalog.toReplaceViewChange(
+ existing, resolveView(schema, "SELECT 2", "new comment"),
Dialects.FLINK);
+
+ Assertions.assertEquals(1, changes.length);
+ Assertions.assertInstanceOf(ViewChange.ReplaceView.class, changes[0]);
+ ViewChange.ReplaceView replaceView = (ViewChange.ReplaceView) changes[0];
+ Assertions.assertEquals("new comment", replaceView.getComment());
+ Representation[] reps = replaceView.getRepresentations();
+ Assertions.assertEquals(1, reps.length);
+ SQLRepresentation sqlRep = (SQLRepresentation) reps[0];
+ Assertions.assertEquals("SELECT 2", sqlRep.sql());
+ Assertions.assertEquals(Dialects.FLINK, sqlRep.dialect());
+ }
+
+ /**
+ * Helper to build a minimal {@link
org.apache.flink.table.catalog.ResolvedCatalogView} for
+ * testing view-change conversion.
+ */
+ private static ResolvedCatalogView resolveView(Schema schema, String query,
String comment) {
+ CatalogView catalogView = CatalogView.of(schema, comment, query, query,
Collections.emptyMap());
+ ResolvedSchema resolvedSchema =
+ ResolvedSchema.of(
+ schema.getColumns().stream()
+ .map(
+ c -> {
+ if (c instanceof Schema.UnresolvedPhysicalColumn) {
+ Schema.UnresolvedPhysicalColumn pc =
(Schema.UnresolvedPhysicalColumn) c;
+ return Column.physical(pc.getName(), DataTypes.INT());
+ }
+ return Column.physical(c.getName(), DataTypes.INT());
+ })
+ .collect(Collectors.toList()));
+ return new ResolvedCatalogView(catalogView, resolvedSchema);
+ }
+
+ /**
+ * Returns a mock {@link Catalog} whose {@code asViewCatalog()} throws
+ * UnsupportedOperationException.
+ */
+ private static Catalog mockUnsupportedViewCatalog() {
+ Catalog gravitinoCatalog = Mockito.mock(Catalog.class);
+ Mockito.when(gravitinoCatalog.asViewCatalog())
+ .thenThrow(new UnsupportedOperationException("views not supported"));
+ return gravitinoCatalog;
}
private static class TestableBaseCatalog extends BaseCatalog {
private final AbstractCatalog delegate;
+ private final Catalog gravitinoCatalog;
- TestableBaseCatalog(AbstractCatalog delegate) {
+ TestableBaseCatalog(AbstractCatalog delegate, Catalog gravitinoCatalog) {
super(
"test",
Collections.emptyMap(),
@@ -158,11 +300,17 @@ public class TestBaseCatalog {
Mockito.mock(SchemaAndTablePropertiesConverter.class),
Mockito.mock(PartitionConverter.class));
this.delegate = delegate;
+ this.gravitinoCatalog = gravitinoCatalog;
}
@Override
protected AbstractCatalog realCatalog() {
return delegate;
}
+
+ @Override
+ protected Catalog catalog() {
+ return gravitinoCatalog;
+ }
}
}
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/hive/FlinkHiveCatalogIT.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/hive/FlinkHiveCatalogIT.java
index 27f60b558c..eac2c9ae3a 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/hive/FlinkHiveCatalogIT.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/hive/FlinkHiveCatalogIT.java
@@ -60,6 +60,7 @@ import
org.apache.flink.table.catalog.hive.factories.HiveCatalogFactoryOptions;
import org.apache.flink.table.catalog.hive.util.Constants;
import org.apache.flink.types.Row;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
import org.apache.gravitino.catalog.hive.HiveConstants;
import org.apache.gravitino.catalog.hive.HiveStorageConstants;
import org.apache.gravitino.flink.connector.CatalogPropertiesConverter;
@@ -71,7 +72,10 @@ import
org.apache.gravitino.flink.connector.store.GravitinoCatalogStoreFactoryOp
import org.apache.gravitino.integration.test.container.ContainerSuite;
import org.apache.gravitino.integration.test.util.TestDatabaseName;
import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.SQLRepresentation;
import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.expressions.transforms.Transforms;
import org.apache.gravitino.rel.types.Types;
@@ -745,6 +749,277 @@ public abstract class FlinkHiveCatalogIT extends
FlinkCommonIT {
true);
}
+ @Test
+ public void testCreateView() {
+ String schemaName = "test_hive_create_view_db";
+ String viewName = "test_view_create";
+ String tableName = "test_view_base_table";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT, name STRING) WITH
('connector'='hive')", tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql(
+ "CREATE VIEW %s COMMENT 'view comment' AS SELECT id, name
FROM %s",
+ viewName, tableName),
+ ResultKind.SUCCESS);
+
+ // Verify via Gravitino ViewCatalog
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+ View view = viewCatalog.loadView(NameIdentifier.of(schemaName,
viewName));
+ Assertions.assertEquals(viewName, view.name());
+ Assertions.assertEquals("view comment", view.comment());
+ Assertions.assertEquals(1, view.representations().length);
+ Assertions.assertInstanceOf(SQLRepresentation.class,
view.representations()[0]);
+
+ // Verify via Flink catalog API
+ Optional<Catalog> flinkCatalog = tableEnv.getCatalog(catalog.name());
+ Assertions.assertTrue(flinkCatalog.isPresent());
+ try {
+ CatalogBaseTable flinkTable =
+ flinkCatalog.get().getTable(new ObjectPath(schemaName,
viewName));
+ Assertions.assertEquals(CatalogBaseTable.TableKind.VIEW,
flinkTable.getTableKind());
+ } catch (TableNotExistException e) {
+ Assertions.fail("view should exist in Flink catalog: " +
e.getMessage());
+ }
+ },
+ true);
+ }
+
+ @Test
+ public void testListViews() {
+ String schemaName = "test_hive_list_views_db";
+ String tableName = "test_list_view_base";
+ String view1 = "test_list_view_1";
+ String view2 = "test_list_view_2";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", view1, tableName),
ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", view2, tableName),
ResultKind.SUCCESS);
+
+ List<String> views = Arrays.asList(tableEnv.listViews());
+ Assertions.assertTrue(views.contains(view1), "view1 not found in
SHOW VIEWS");
+ Assertions.assertTrue(views.contains(view2), "view2 not found in
SHOW VIEWS");
+
+ // Tables should not appear in listViews
+ Assertions.assertFalse(views.contains(tableName), "table should not
appear in listViews");
+
+ // Verify via Gravitino ViewCatalog
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+ NameIdentifier[] gravitinoViews =
viewCatalog.listViews(Namespace.of(schemaName));
+ List<String> gravitinoViewNames =
+
Arrays.stream(gravitinoViews).map(NameIdentifier::name).collect(Collectors.toList());
+ Assertions.assertTrue(gravitinoViewNames.contains(view1));
+ Assertions.assertTrue(gravitinoViewNames.contains(view2));
+ },
+ true);
+ }
+
+ @Test
+ public void testDropView() {
+ String schemaName = "test_hive_drop_view_db";
+ String tableName = "test_drop_view_base";
+ String viewName = "test_view_drop";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+ Assertions.assertTrue(
+ viewCatalog.viewExists(NameIdentifier.of(schemaName, viewName)),
+ "view should exist before drop");
+
+ TestUtils.assertTableResult(sql("DROP VIEW %s", viewName),
ResultKind.SUCCESS);
+
+ Assertions.assertFalse(
+ viewCatalog.viewExists(NameIdentifier.of(schemaName, viewName)),
+ "view should not exist after drop");
+ },
+ true);
+ }
+
+ @Test
+ public void testAlterViewRename() {
+ String schemaName = "test_hive_rename_view_db";
+ String tableName = "test_rename_view_base";
+ String viewName = "test_view_rename_src";
+ String newViewName = "test_view_rename_dst";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ TestUtils.assertTableResult(
+ sql("ALTER VIEW %s RENAME TO %s", viewName, newViewName),
ResultKind.SUCCESS);
+
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+ Assertions.assertFalse(
+ viewCatalog.viewExists(NameIdentifier.of(schemaName, viewName)),
+ "old view name should not exist");
+ Assertions.assertTrue(
+ viewCatalog.viewExists(NameIdentifier.of(schemaName,
newViewName)),
+ "new view name should exist");
+ },
+ true);
+ }
+
+ @Test
+ public void testAlterViewReplaceBody() {
+ String schemaName = "test_hive_replace_view_db";
+ String tableName = "test_replace_view_base";
+ String viewName = "test_view_replace";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT, name STRING) WITH
('connector'='hive')", tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ // Replace the view body to select a different column
+ TestUtils.assertTableResult(
+ sql("ALTER VIEW %s AS SELECT id, name FROM %s", viewName,
tableName),
+ ResultKind.SUCCESS);
+
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+ View view = viewCatalog.loadView(NameIdentifier.of(schemaName,
viewName));
+ Assertions.assertEquals(1, view.representations().length);
+ SQLRepresentation rep = (SQLRepresentation)
view.representations()[0];
+ Assertions.assertTrue(
+ rep.sql().contains("name") && rep.sql().contains("id"),
+ "updated view SQL should select both id and name columns");
+ },
+ true);
+ }
+
+ @Test
+ public void testQueryView() {
+ String schemaName = "test_hive_query_view_db";
+ String tableName = "test_query_view_base";
+ String viewName = "test_view_query";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT, name STRING) WITH
('connector'='hive')", tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id, name FROM %s WHERE id > 1",
viewName, tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("INSERT INTO %s VALUES (1, 'alice'), (2, 'bob'), (3,
'carol')", tableName),
+ ResultKind.SUCCESS_WITH_CONTENT,
+ Row.of(-1L));
+
+ TestUtils.assertTableResult(
+ sql("SELECT * FROM %s ORDER BY id", viewName),
+ ResultKind.SUCCESS_WITH_CONTENT,
+ Row.of(2, "bob"),
+ Row.of(3, "carol"));
+ },
+ true);
+ }
+
+ @Test
+ public void testCreateViewIfNotExists() {
+ String schemaName = "test_hive_create_view_ifte_db";
+ String tableName = "test_create_view_ifte_base";
+ String viewName = "test_view_ifte";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ // Second CREATE VIEW IF NOT EXISTS should succeed without error
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW IF NOT EXISTS %s AS SELECT id FROM %s",
viewName, tableName),
+ ResultKind.SUCCESS);
+
+ // View should still exist and be unchanged
+ ViewCatalog viewCatalog = catalog.asViewCatalog();
+
Assertions.assertTrue(viewCatalog.viewExists(NameIdentifier.of(schemaName,
viewName)));
+ },
+ true);
+ }
+
+ @Test
+ public void testDropViewIfExists() {
+ String schemaName = "test_hive_drop_view_ifte_db";
+ String tableName = "test_drop_view_ifte_base";
+ String viewName = "test_view_drop_ifte";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ TestUtils.assertTableResult(sql("DROP VIEW %s", viewName),
ResultKind.SUCCESS);
+
+ // DROP VIEW IF EXISTS on a non-existent view should not throw
+ TestUtils.assertTableResult(sql("DROP VIEW IF EXISTS %s", viewName),
ResultKind.SUCCESS);
+ },
+ true);
+ }
+
+ @Test
+ public void testListTablesDoesNotIncludeViews() {
+ String schemaName = "test_hive_list_tables_no_views_db";
+ String tableName = "test_list_no_view_base";
+ String viewName = "test_list_no_view_view";
+ doWithSchema(
+ currentCatalog(),
+ schemaName,
+ catalog -> {
+ TestUtils.assertTableResult(
+ sql("CREATE TABLE %s (id INT) WITH ('connector'='hive')",
tableName),
+ ResultKind.SUCCESS);
+ TestUtils.assertTableResult(
+ sql("CREATE VIEW %s AS SELECT id FROM %s", viewName, tableName),
ResultKind.SUCCESS);
+
+ List<String> tables = Arrays.asList(tableEnv.listTables());
+ Assertions.assertTrue(tables.contains(tableName), "table should
appear in listTables");
+ Assertions.assertFalse(tables.contains(viewName), "view should not
appear in listTables");
+
+ List<String> views = Arrays.asList(tableEnv.listViews());
+ Assertions.assertTrue(views.contains(viewName), "view should appear
in listViews");
+ Assertions.assertFalse(views.contains(tableName), "table should not
appear in listViews");
+ },
+ true);
+ }
+
private void assertHiveCatalogRead(String databaseName, String tableName,
Row expectedRow) {
EnvironmentSettings settings =
EnvironmentSettings.newInstance().inBatchMode().build();
TableEnvironment hiveEnv = TableEnvironment.create(settings);