This is an automated email from the ASF dual-hosted git repository.
diqiu50 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 5d40ec4403 [#11926] Encode Trino dialect Hive views using Trino's
native Presto View format (#12294)
5d40ec4403 is described below
commit 5d40ec44034995a3652fc7ad773b78a9acf1b1ce
Author: Yuhui <[email protected]>
AuthorDate: Mon Aug 17 10:47:01 2026 +0800
[#11926] Encode Trino dialect Hive views using Trino's native Presto View
format (#12294)
### What changes were proposed in this pull request?
Store and read Trino dialect views in the Hive catalog using Trino's own
native "Presto View" Hive Metastore encoding (via a new
`TrinoNativeViewCodec`), instead of the previous custom
marker/prefix-based encoding.
### Why are the changes needed?
So Hive-backed views created through Gravitino are interoperable with a
native Trino/Presto Hive connector pointed at the same Hive Metastore,
and vice versa. This is the catalog-side (non-Trino-connector) portion
of #11926, split out for independent review.
### Does this PR introduce _any_ user-facing change?
No behavior change for existing non-Trino views; Trino dialect views
stored via the Hive catalog now use Trino's native encoding format.
### How was this patch tested?
Existing and new unit tests in `catalog-hive`
(`TestHiveCatalogOperations`, `TestTrinoNativeViewCodec`).
---
catalogs/catalog-hive/build.gradle.kts | 1 +
.../catalog/hive/HiveTablePropertiesMetadata.java | 5 +
.../apache/gravitino/catalog/hive/HiveView.java | 33 +-
.../catalog/hive/HiveViewCatalogOperations.java | 273 +++++++-
.../catalog/hive/TrinoNativeViewCodec.java | 460 +++++++++++++
.../catalog/hive/TestHiveCatalogOperations.java | 741 ++++++++++++++++++++-
.../catalog/hive/TestTrinoNativeViewCodec.java | 318 +++++++++
docs/apache-hive-catalog.md | 6 +-
8 files changed, 1767 insertions(+), 70 deletions(-)
diff --git a/catalogs/catalog-hive/build.gradle.kts
b/catalogs/catalog-hive/build.gradle.kts
index 35bf217757..6f913cb9aa 100644
--- a/catalogs/catalog-hive/build.gradle.kts
+++ b/catalogs/catalog-hive/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
implementation(libs.commons.io)
implementation(libs.commons.lang3)
implementation(libs.guava)
+ implementation(libs.jackson.databind)
implementation(libs.hadoop3.auth) {
exclude("*")
}
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveTablePropertiesMetadata.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveTablePropertiesMetadata.java
index fb4bd8ea15..48fa40c9e9 100644
---
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveTablePropertiesMetadata.java
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/HiveTablePropertiesMetadata.java
@@ -50,6 +50,11 @@ public class HiveTablePropertiesMetadata extends
BasePropertiesMetadata {
List<PropertyEntry<?>> propertyEntries =
ImmutableList.of(
stringReservedPropertyEntry(COMMENT, "table comment", true),
+ stringReservedPropertyEntry(
+ TrinoNativeViewCodec.PRESTO_VIEW_FLAG,
+ "Marks a view stored using Trino's native \"Presto View\" HMS
format; managed "
+ + "internally by the Trino dialect view encoding, not
settable by users",
+ true),
stringReservedPropertyEntry(NUM_FILES, "number of files", false),
stringReservedPropertyEntry(TOTAL_SIZE, "total size of the table",
false),
booleanReservedPropertyEntry(
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 a2aeee7a0b..a3d8da8350 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
@@ -40,9 +40,12 @@ 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}, Flink views carry properties prefixed with
{@code flink.}, and all
- * other views are treated as native Hive SQL views.
+ * from table properties: Trino views carry the {@code presto_view} HMS
property with a {@code
+ * comment} of {@code "Presto View"} and are encoded using Trino's own native
"Presto View" format
+ * (see {@link TrinoNativeViewCodec}), so this catalog is interoperable with
views created directly
+ * by a native Trino/Presto Hive connector, not only ones created through
Gravitino. Spark views
+ * carry {@code 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)
@@ -54,8 +57,6 @@ public class HiveView implements View {
static final String SPARK_VERSION_KEY = "spark.sql.create.version";
static final String FLINK_PROPERTY_PREFIX = "flink.";
- private static final String TRINO_VIEW_MARKER_KEY = "presto_view";
- private static final String TRINO_VIEW_PREFIX = "/* Presto View:";
private String name;
private String comment;
@@ -109,18 +110,24 @@ public class HiveView implements View {
}
/**
- * Detects the SQL dialect from HMS table properties and view text.
+ * Detects the SQL dialect from HMS table properties.
*
- * @param viewOriginalText The original view text from HMS.
* @param parameters The HMS table parameters map.
* @return The detected dialect string: "trino", "spark", "flink", or "hive".
+ * @throws UnsupportedOperationException if the view carries the {@code
presto_view} marker but is
+ * not a plain Trino view (e.g. a Trino/Presto materialized view); its
encoded body cannot be
+ * read as SQL by any engine.
*/
- static String detectDialect(String viewOriginalText, Map<String, String>
parameters) {
- if (parameters != null &&
"true".equalsIgnoreCase(parameters.get(TRINO_VIEW_MARKER_KEY))) {
- return Dialects.TRINO;
- }
- if (StringUtils.startsWith(viewOriginalText, TRINO_VIEW_PREFIX)) {
- return Dialects.TRINO;
+ static String detectDialect(Map<String, String> parameters) {
+ if (parameters != null
+ &&
"true".equalsIgnoreCase(parameters.get(TrinoNativeViewCodec.PRESTO_VIEW_FLAG)))
{
+ if (TrinoNativeViewCodec.PRESTO_VIEW_COMMENT.equalsIgnoreCase(
+ parameters.get(HiveConstants.COMMENT))) {
+ return Dialects.TRINO;
+ }
+ throw new UnsupportedOperationException(
+ "View carries the presto_view marker but is not a plain Trino view
(e.g. a Trino/Presto "
+ + "materialized view); its encoded body cannot be read by
Gravitino");
}
if (parameters != null && parameters.containsKey(SPARK_VERSION_KEY)) {
return Dialects.SPARK;
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 f3d078ff2e..7fcd812e5e 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
@@ -26,11 +26,14 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.NameIdentifier;
@@ -50,12 +53,33 @@ import org.apache.gravitino.rel.SQLRepresentation;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.rel.ViewChange;
+import org.apache.gravitino.rel.types.Types;
import org.apache.gravitino.utils.PrincipalUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class HiveViewCatalogOperations implements ViewCatalog {
private static final Logger LOG =
LoggerFactory.getLogger(HiveViewCatalogOperations.class);
+ private static final String SUPPORTED_VIEW_DIALECTS =
+ String.join(", ", Dialects.HIVE, Dialects.TRINO, Dialects.FLINK,
Dialects.SPARK);
+
+ /**
+ * The HMS-level representation derived from a logical view definition: the
columns, comment, and
+ * {@code viewOriginalText} actually stored on the underlying HMS table. For
a Trino dialect view
+ * these differ from the caller-supplied values (see {@link
#encodeHmsView}); for every other
+ * dialect they are passed through unchanged.
+ */
+ private static final class HmsViewEncoding {
+ final Column[] columns;
+ final String comment;
+ final String viewOriginalText;
+
+ HmsViewEncoding(Column[] columns, String comment, String viewOriginalText)
{
+ this.columns = columns;
+ this.comment = comment;
+ this.viewOriginalText = viewOriginalText;
+ }
+ }
private final Supplier<CachedClientPool> clientPoolSupplier;
private final Supplier<String> catalogNameSupplier;
@@ -116,18 +140,20 @@ class HiveViewCatalogOperations implements ViewCatalog {
Map<String, String> safeProperties = properties == null ?
ImmutableMap.of() : properties;
SQLRepresentation sqlRepresentation =
validateSQLRepresentation(
- representations, defaultCatalog, defaultSchema, safeProperties,
ident);
+ representations, defaultCatalog, defaultSchema, safeProperties,
columns, ident);
try {
Map<String, String> params = Maps.newHashMap(safeProperties);
params.put(TABLE_TYPE, TableType.VIRTUAL_VIEW.name());
- String viewOriginalText = toHmsViewOriginalText(sqlRepresentation,
ident);
+ HmsViewEncoding encoding =
+ encodeHmsView(
+ sqlRepresentation, columns, comment, defaultCatalog,
defaultSchema, params, ident);
HiveTable hiveTable =
HiveTable.builder()
.withName(ident.name())
- .withComment(comment)
- .withColumns(copyColumns(columns))
+ .withComment(encoding.comment)
+ .withColumns(encoding.columns)
.withProperties(params)
.withAuditInfo(
AuditInfo.builder()
@@ -136,7 +162,7 @@ class HiveViewCatalogOperations implements ViewCatalog {
.build())
.withCatalogName(catalogName())
.withDatabaseName(schemaIdent.name())
- .withViewOriginalText(viewOriginalText)
+ .withViewOriginalText(encoding.viewOriginalText)
.build();
clientPool()
@@ -177,6 +203,34 @@ class HiveViewCatalogOperations implements ViewCatalog {
throw new NoSuchViewException("No view named %s (it is a table, not a
view)", ident.name());
}
+ // Reuse the same dialect detection as loadHiveView()/toHiveView() so
that a presto_view
+ // entry that is not a plain Trino view (e.g. a Trino/Presto
materialized view) is rejected
+ // here too, instead of being silently treated as a non-Trino view.
+ boolean isTrinoView =
+
Dialects.TRINO.equalsIgnoreCase(HiveView.detectDialect(currentHiveTable.properties()));
+ // Gravitino's view model has no owner/runAsInvoker/path concept, so
replacing a native Trino
+ // view that carries a non-default value for any of them would silently
discard it (e.g. a
+ // SECURITY DEFINER view with an owner would silently become an
ownerless SECURITY INVOKER
+ // view). Reject the replace instead of doing that.
+ boolean currentTrinoViewHasUnrepresentableFields = false;
+ if (isTrinoView) {
+ TrinoNativeViewCodec.ViewDefinition currentDefinition;
+ try {
+ currentDefinition =
TrinoNativeViewCodec.decode(currentHiveTable.viewOriginalText());
+ } catch (IllegalArgumentException e) {
+ throw new UnsupportedOperationException(
+ "View "
+ + ident
+ + " carries the presto_view marker but its payload cannot be
"
+ + "decoded",
+ e);
+ }
+ currentTrinoViewHasUnrepresentableFields =
+ currentDefinition.owner != null
+ || !currentDefinition.runAsInvoker
+ || !currentDefinition.path.isEmpty();
+ }
+
String newViewName = currentHiveTable.name();
String updatedViewOriginalText = currentHiveTable.viewOriginalText();
Map<String, String> updatedProperties =
Maps.newHashMap(currentHiveTable.properties());
@@ -196,18 +250,48 @@ class HiveViewCatalogOperations implements ViewCatalog {
} else if (change instanceof ViewChange.SetProperty) {
ViewChange.SetProperty sp = (ViewChange.SetProperty) change;
if (COMMENT.equals(sp.getProperty())) {
+ if (isTrinoView) {
+ throw new UnsupportedOperationException(
+ "Trino dialect views store their comment inside the encoded
view payload; use "
+ + "ReplaceView to change it, not SetProperty(comment)");
+ }
updatedComment = sp.getValue();
+ } else if
(TrinoNativeViewCodec.PRESTO_VIEW_FLAG.equals(sp.getProperty())) {
+ throw new UnsupportedOperationException(
+ "Property '"
+ + TrinoNativeViewCodec.PRESTO_VIEW_FLAG
+ + "' is reserved for native Trino view storage and cannot
be set directly; "
+ + "use ReplaceView to change the view's dialect");
} else {
updatedProperties.put(sp.getProperty(), sp.getValue());
}
} else if (change instanceof ViewChange.RemoveProperty) {
String property = ((ViewChange.RemoveProperty) change).getProperty();
if (COMMENT.equals(property)) {
+ if (isTrinoView) {
+ throw new UnsupportedOperationException(
+ "Trino dialect views store their comment inside the encoded
view payload; use "
+ + "ReplaceView to change it, not
RemoveProperty(comment)");
+ }
updatedComment = null;
+ } else if (TrinoNativeViewCodec.PRESTO_VIEW_FLAG.equals(property)) {
+ throw new UnsupportedOperationException(
+ "Property '"
+ + TrinoNativeViewCodec.PRESTO_VIEW_FLAG
+ + "' is reserved for native Trino view storage and cannot
be removed "
+ + "directly; use ReplaceView to change the view's
dialect");
} else {
updatedProperties.remove(property);
}
} else if (change instanceof ViewChange.ReplaceView) {
+ if (currentTrinoViewHasUnrepresentableFields) {
+ throw new UnsupportedOperationException(
+ "View "
+ + ident
+ + " is a native Trino view with a non-default owner,
runAsInvoker, or SQL "
+ + "path; Gravitino cannot represent these fields, so
replacing it would "
+ + "silently discard them");
+ }
ViewChange.ReplaceView replace = (ViewChange.ReplaceView) change;
SQLRepresentation sqlRepresentation =
validateSQLRepresentation(
@@ -215,10 +299,21 @@ class HiveViewCatalogOperations implements ViewCatalog {
replace.getDefaultCatalog(),
replace.getDefaultSchema(),
updatedProperties,
+ replace.getColumns(),
ident);
- updatedColumns = copyColumns(replace.getColumns());
- updatedComment = replace.getComment();
- updatedViewOriginalText = toHmsViewOriginalText(sqlRepresentation,
ident);
+ HmsViewEncoding encoding =
+ encodeHmsView(
+ sqlRepresentation,
+ replace.getColumns(),
+ replace.getComment(),
+ replace.getDefaultCatalog(),
+ replace.getDefaultSchema(),
+ updatedProperties,
+ ident);
+ updatedColumns = encoding.columns;
+ updatedViewOriginalText = encoding.viewOriginalText;
+ updatedComment = encoding.comment;
+ isTrinoView =
Dialects.TRINO.equalsIgnoreCase(sqlRepresentation.dialect());
} else {
throw new IllegalArgumentException(
"Unsupported view change type: " +
change.getClass().getSimpleName());
@@ -324,6 +419,11 @@ class HiveViewCatalogOperations implements ViewCatalog {
return true;
} catch (NoSuchViewException e) {
return false;
+ } catch (UnsupportedOperationException e) {
+ // The HMS entry exists but Gravitino cannot fully interpret it (e.g. a
materialized view or
+ // an undecodable native Trino view payload); treat it as existing so
callers (e.g. a rename
+ // target check) don't collide with it.
+ return true;
}
}
@@ -364,19 +464,52 @@ class HiveViewCatalogOperations implements ViewCatalog {
AuditInfo auditInfo) {
Map<String, String> params =
Maps.newHashMap(properties != null ? properties : ImmutableMap.of());
- String representationSql = viewOriginalText;
- String detectedDialect = HiveView.detectDialect(representationSql, params);
+ String detectedDialect = HiveView.detectDialect(params);
switch (detectedDialect.toLowerCase(Locale.ROOT)) {
case Dialects.HIVE:
+ case Dialects.TRINO:
case Dialects.FLINK:
case Dialects.SPARK:
break;
default:
- // TODO(design-docs/gravitino-logical-view-management.md): support
loading trino HMS views.
throw new UnsupportedOperationException(
String.format(
- "Hive catalog currently supports only '%s', '%s' and '%s' view
dialects, but found '%s' for view %s",
- Dialects.HIVE, Dialects.FLINK, Dialects.SPARK,
detectedDialect, ident));
+ "Hive catalog currently supports only [%s] view dialects, but
found '%s' for view %s",
+ SUPPORTED_VIEW_DIALECTS, detectedDialect, ident));
+ }
+
+ String representationSql;
+ String resolvedComment;
+ String restoredDefaultCatalog = null;
+ String restoredDefaultSchema = null;
+ Column[] resolvedColumns;
+ if (Dialects.TRINO.equalsIgnoreCase(detectedDialect)) {
+ // Trino dialect views are encoded using Trino's native "Presto View"
format, so the SQL,
+ // comment, default catalog/schema, and real columns all live in the
encoded payload; the
+ // underlying HMS table only carries a single dummy column (see
hmsColumns()).
+ TrinoNativeViewCodec.ViewDefinition decoded;
+ try {
+ decoded = TrinoNativeViewCodec.decode(viewOriginalText);
+ } catch (IllegalArgumentException e) {
+ throw new UnsupportedOperationException(
+ "View " + ident + " carries the presto_view marker but its payload
cannot be decoded",
+ e);
+ }
+ representationSql = decoded.originalSql;
+ resolvedComment = decoded.comment;
+ restoredDefaultCatalog = decoded.catalog;
+ restoredDefaultSchema = decoded.schema;
+ resolvedColumns =
+ decoded.columns.stream()
+ .map(
+ c ->
+ Column.of(
+ c.name,
TrinoNativeViewCodec.fromTrinoTypeString(c.type), c.comment))
+ .toArray(Column[]::new);
+ } else {
+ representationSql = viewOriginalText;
+ resolvedComment = comment;
+ resolvedColumns = copyColumns(columns);
}
SQLRepresentation rep =
@@ -387,11 +520,13 @@ class HiveViewCatalogOperations implements ViewCatalog {
return HiveView.builder()
.withName(ident.name())
- .withComment(comment)
- .withColumns(copyColumns(columns))
+ .withComment(resolvedComment)
+ .withColumns(resolvedColumns)
.withRepresentations(new SQLRepresentation[] {rep})
.withProperties(params)
.withAuditInfo(auditInfo)
+ .withDefaultCatalog(restoredDefaultCatalog)
+ .withDefaultSchema(restoredDefaultSchema)
.build();
}
@@ -407,6 +542,7 @@ class HiveViewCatalogOperations implements ViewCatalog {
String defaultCatalog,
String defaultSchema,
Map<String, String> properties,
+ Column[] columns,
NameIdentifier ident) {
int representationCount = representations == null ? 0 :
representations.length;
Representation firstRepresentation =
@@ -431,6 +567,23 @@ class HiveViewCatalogOperations implements ViewCatalog {
defaultSchema,
ident);
return selected;
+ case Dialects.TRINO:
+ // The default catalog/schema are encoded into the Trino native view
payload by
+ // toHmsViewOriginalText() and restored in toHiveView(), so no value
is required to be null
+ // here.
+ Preconditions.checkArgument(
+ columns != null && columns.length > 0,
+ "Dialect '%s' requires at least one column for view %s; without it
the encoded "
+ + "payload cannot be decoded on the next load",
+ selected.dialect(),
+ ident);
+ Preconditions.checkArgument(
+ defaultSchema == null || defaultCatalog != null,
+ "Dialect '%s' does not support a defaultSchema without a
defaultCatalog for view "
+ + "%s, since Trino's native view format rejects such a
payload",
+ selected.dialect(),
+ ident);
+ return selected;
case Dialects.FLINK:
Preconditions.checkArgument(
defaultCatalog == null && defaultSchema == null,
@@ -457,27 +610,88 @@ class HiveViewCatalogOperations implements ViewCatalog {
HiveView.SPARK_VERSION_KEY);
return selected;
default:
- // TODO(design-docs/gravitino-logical-view-management.md): support
creating trino HMS views.
throw new UnsupportedOperationException(
String.format(
- "Hive catalog currently supports only '%s', '%s' and '%s' view
dialects, but got '%s' for view %s",
- Dialects.HIVE, Dialects.FLINK, Dialects.SPARK,
selected.dialect(), ident));
+ "Hive catalog currently supports only [%s] view dialects, but
got '%s' for view %s",
+ SUPPORTED_VIEW_DIALECTS, selected.dialect(), ident));
+ }
+ }
+
+ /**
+ * Sets or clears the {@code presto_view} marker in the given HMS property
map, so that a Trino
+ * dialect view is recognized as a native Trino view (see {@link
TrinoNativeViewCodec}).
+ */
+ private void applyTrinoViewMarker(Map<String, String> params, String
dialect) {
+ if (!Dialects.TRINO.equalsIgnoreCase(dialect)) {
+ params.remove(TrinoNativeViewCodec.PRESTO_VIEW_FLAG);
+ return;
}
+ params.put(TrinoNativeViewCodec.PRESTO_VIEW_FLAG, "true");
}
- private String toHmsViewOriginalText(SQLRepresentation representation,
NameIdentifier ident) {
+ /**
+ * Derives the HMS-level representation of a view from its logical
definition, shared by {@link
+ * #createView} and the {@code ReplaceView} branch of {@link #alterView}.
The caller supplies the
+ * logical SQL representation, output columns, user comment, and default
catalog/schema; {@code
+ * properties} is mutated in place to set or clear the {@code presto_view}
marker.
+ */
+ private HmsViewEncoding encodeHmsView(
+ SQLRepresentation sqlRepresentation,
+ Column[] columns,
+ String comment,
+ String defaultCatalog,
+ String defaultSchema,
+ Map<String, String> properties,
+ NameIdentifier ident) {
+ applyTrinoViewMarker(properties, sqlRepresentation.dialect());
+ String viewOriginalText =
+ toHmsViewOriginalText(
+ sqlRepresentation, columns, comment, defaultCatalog,
defaultSchema, ident);
+ String hmsComment =
+ Dialects.TRINO.equalsIgnoreCase(sqlRepresentation.dialect())
+ ? TrinoNativeViewCodec.PRESTO_VIEW_COMMENT
+ : comment;
+ return new HmsViewEncoding(
+ hmsColumns(columns, sqlRepresentation.dialect()), hmsComment,
viewOriginalText);
+ }
+
+ private String toHmsViewOriginalText(
+ SQLRepresentation representation,
+ Column[] columns,
+ String comment,
+ String defaultCatalog,
+ String defaultSchema,
+ NameIdentifier ident) {
switch (representation.dialect().toLowerCase(Locale.ROOT)) {
case Dialects.HIVE:
case Dialects.FLINK:
case Dialects.SPARK:
return representation.sql();
+ case Dialects.TRINO:
+ List<TrinoNativeViewCodec.ViewColumn> viewColumns =
+ Arrays.stream(columns == null ? new Column[0] : columns)
+ .map(
+ c ->
+ new TrinoNativeViewCodec.ViewColumn(
+ c.name(),
+
TrinoNativeViewCodec.toTrinoTypeString(c.dataType()),
+ c.comment()))
+ .collect(Collectors.toList());
+ return TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ representation.sql(),
+ defaultCatalog,
+ defaultSchema,
+ viewColumns,
+ comment,
+ /* owner= */ null,
+ /* runAsInvoker= */ true,
+ /* path= */ Collections.emptyList()));
default:
- // TODO(design-docs/gravitino-logical-view-management.md): support
serializing trino HMS
- // view definitions.
throw new UnsupportedOperationException(
String.format(
- "Hive catalog currently supports only '%s', '%s' and '%s' view
dialects, but got '%s' for view %s",
- Dialects.HIVE, Dialects.FLINK, Dialects.SPARK,
representation.dialect(), ident));
+ "Hive catalog currently supports only [%s] view dialects, but
got '%s' for view %s",
+ SUPPORTED_VIEW_DIALECTS, representation.dialect(), ident));
}
}
@@ -494,6 +708,19 @@ class HiveViewCatalogOperations implements ViewCatalog {
return columns == null ? new Column[0] : columns.clone();
}
+ /**
+ * Builds the columns to store on the underlying HMS table. Trino dialect
views store their real
+ * columns inside the encoded Trino native view payload (see {@link
#toHmsViewOriginalText}), so
+ * the HMS table itself only carries a single dummy column, matching real
Trino's own behavior
+ * (see {@code io.trino.plugin.hive.HiveMetadata#createView}).
+ */
+ private Column[] hmsColumns(Column[] columns, String dialect) {
+ if (Dialects.TRINO.equalsIgnoreCase(dialect)) {
+ return new Column[] {Column.of("dummy", Types.StringType.get())};
+ }
+ return copyColumns(columns);
+ }
+
private CachedClientPool clientPool() {
return clientPoolSupplier.get();
}
diff --git
a/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java
new file mode 100644
index 0000000000..8962aa67d6
--- /dev/null
+++
b/catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java
@@ -0,0 +1,460 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog.hive;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Locale;
+import javax.annotation.Nullable;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+
+/**
+ * Encodes and decodes Trino/Presto's native "Presto View" HMS view format, so
that views created by
+ * Gravitino's Trino dialect are readable by a native Trino Hive connector
(and vice versa).
+ *
+ * <p>Trino recognizes a Hive Metastore VIRTUAL_VIEW table as a Trino view
when it carries the
+ * {@code presto_view=true} table parameter and its {@code comment} table
parameter equals {@code
+ * "Presto View"}; the view body is stored in {@code viewOriginalText} as
{@code "/* Presto View: "
+ * + base64(json) + " * /"}. The JSON payload mirrors Trino's {@code
ConnectorViewDefinition} wire
+ * format (see {@code io.trino.plugin.hive.ViewReaderUtil} /{@code
+ * io.trino.spi.connector.ConnectorViewDefinition} in the Trino source tree):
{@code Optional}
+ * fields are serialized as the raw value or JSON {@code null}, never omitted,
since Trino's decoder
+ * requires every field to be present.
+ */
+final class TrinoNativeViewCodec {
+
+ static final String PRESTO_VIEW_FLAG = "presto_view";
+ static final String PRESTO_VIEW_COMMENT = "Presto View";
+
+ private static final String VIEW_PREFIX = "/* Presto View: ";
+ private static final String VIEW_SUFFIX = " */";
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ // Trino's default time/timestamp precision (milliseconds) when none is
specified.
+ private static final int DEFAULT_PRECISION = 3;
+
+ private TrinoNativeViewCodec() {}
+
+ /** A single view output column, mirroring Trino's {@code
ConnectorViewDefinition.ViewColumn}. */
+ static final class ViewColumn {
+ final String name;
+ final String type;
+ @Nullable final String comment;
+
+ ViewColumn(String name, String type, @Nullable String comment) {
+ this.name = name;
+ this.type = type;
+ this.comment = comment;
+ }
+ }
+
+ /** Mirrors the fields of Trino's {@code ConnectorViewDefinition}. */
+ static final class ViewDefinition {
+ final String originalSql;
+ @Nullable final String catalog;
+ @Nullable final String schema;
+ final List<ViewColumn> columns;
+ @Nullable final String comment;
+ @Nullable final String owner;
+ final boolean runAsInvoker;
+ // Trino's SQL path setting for the view; Gravitino's view model has no
equivalent, so this is
+ // only populated on decode() (to let callers detect and reject a
non-empty path) and is always
+ // written back empty by encode().
+ final List<String> path;
+
+ ViewDefinition(
+ String originalSql,
+ @Nullable String catalog,
+ @Nullable String schema,
+ List<ViewColumn> columns,
+ @Nullable String comment,
+ @Nullable String owner,
+ boolean runAsInvoker,
+ List<String> path) {
+ this.originalSql = originalSql;
+ this.catalog = catalog;
+ this.schema = schema;
+ this.columns = columns;
+ this.comment = comment;
+ this.owner = owner;
+ this.runAsInvoker = runAsInvoker;
+ this.path = path;
+ }
+ }
+
+ /**
+ * Encodes a view definition into Trino's native {@code viewOriginalText}
format.
+ *
+ * @param definition the view definition to encode
+ * @return the encoded {@code "/* Presto View: ... * /"} string
+ */
+ static String encode(ViewDefinition definition) {
+ ObjectNode root = MAPPER.createObjectNode();
+ root.put("originalSql", definition.originalSql);
+ root.put("catalog", definition.catalog);
+ root.put("schema", definition.schema);
+
+ ArrayNode columns = root.putArray("columns");
+ for (ViewColumn column : definition.columns) {
+ ObjectNode columnNode = columns.addObject();
+ columnNode.put("name", column.name);
+ columnNode.put("type", column.type);
+ columnNode.put("comment", column.comment);
+ }
+
+ root.put("comment", definition.comment);
+ root.put("owner", definition.owner);
+ root.put("runAsInvoker", definition.runAsInvoker);
+ root.putArray("path");
+
+ byte[] bytes;
+ try {
+ bytes = MAPPER.writeValueAsBytes(root);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to encode Trino native view
definition", e);
+ }
+ return VIEW_PREFIX + Base64.getEncoder().encodeToString(bytes) +
VIEW_SUFFIX;
+ }
+
+ /**
+ * Decodes a Trino native {@code viewOriginalText} value.
+ *
+ * @param viewOriginalText the raw {@code viewOriginalText} HMS field value
+ * @return the decoded view definition
+ */
+ static ViewDefinition decode(String viewOriginalText) {
+ if (viewOriginalText == null
+ || !viewOriginalText.startsWith(VIEW_PREFIX)
+ || !viewOriginalText.endsWith(VIEW_SUFFIX)) {
+ throw new IllegalArgumentException(
+ "Not a valid Trino native view: viewOriginalText is missing the
Presto View prefix/suffix");
+ }
+ String encoded =
+ viewOriginalText.substring(
+ VIEW_PREFIX.length(), viewOriginalText.length() -
VIEW_SUFFIX.length());
+ byte[] bytes = Base64.getDecoder().decode(encoded);
+
+ JsonNode root;
+ try {
+ root = MAPPER.readTree(bytes);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to decode Trino native view
definition", e);
+ }
+
+ JsonNode columnsNode = root.path("columns");
+ if (!columnsNode.isArray() || columnsNode.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Not a valid Trino native view: 'columns' field is missing, not an
array, or empty");
+ }
+ List<ViewColumn> columns = new ArrayList<>();
+ for (JsonNode columnNode : columnsNode) {
+ String name = textOrNull(columnNode, "name");
+ String type = textOrNull(columnNode, "type");
+ if (name == null || type == null) {
+ throw new IllegalArgumentException(
+ "Not a valid Trino native view: a column is missing 'name' or
'type'");
+ }
+ columns.add(new ViewColumn(name, type, textOrNull(columnNode,
"comment")));
+ }
+
+ String originalSql = textOrNull(root, "originalSql");
+ if (originalSql == null) {
+ throw new IllegalArgumentException(
+ "Not a valid Trino native view: 'originalSql' field is missing or
null");
+ }
+
+ List<String> path = new ArrayList<>();
+ for (JsonNode pathNode : root.path("path")) {
+ path.add(pathNode.asText());
+ }
+
+ return new ViewDefinition(
+ originalSql,
+ textOrNull(root, "catalog"),
+ textOrNull(root, "schema"),
+ columns,
+ textOrNull(root, "comment"),
+ textOrNull(root, "owner"),
+ root.path("runAsInvoker").asBoolean(true),
+ path);
+ }
+
+ @Nullable
+ private static String textOrNull(JsonNode node, String field) {
+ JsonNode value = node.get(field);
+ return value == null || value.isNull() ? null : value.asText();
+ }
+
+ /**
+ * Converts a Gravitino type to its Trino type signature string (e.g. {@code
varchar(10)}, {@code
+ * row(a integer,b varchar)}), for use in the encoded view's column list.
+ *
+ * @param type the Gravitino type
+ * @return the Trino type signature string
+ */
+ static String toTrinoTypeString(Type type) {
+ switch (type.name()) {
+ case BOOLEAN:
+ return "boolean";
+ case BYTE:
+ // Trino has no unsigned integer types; widen to the next size up to
preserve range,
+ // matching GeneralDataTypeTransformer's Gravitino-to-Trino type
mapping.
+ return ((Types.ByteType) type).signed() ? "tinyint" : "smallint";
+ case SHORT:
+ return ((Types.ShortType) type).signed() ? "smallint" : "integer";
+ case INTEGER:
+ return ((Types.IntegerType) type).signed() ? "integer" : "bigint";
+ case LONG:
+ return ((Types.LongType) type).signed() ? "bigint" : "decimal(20,0)";
+ case FLOAT:
+ return "real";
+ case DOUBLE:
+ return "double";
+ case STRING:
+ return "varchar";
+ case VARCHAR:
+ return "varchar(" + ((Types.VarCharType) type).length() + ")";
+ case FIXEDCHAR:
+ return "char(" + ((Types.FixedCharType) type).length() + ")";
+ case DATE:
+ return "date";
+ case TIME:
+ Types.TimeType timeType = (Types.TimeType) type;
+ int timePrecision = timeType.hasPrecisionSet() ? timeType.precision()
: DEFAULT_PRECISION;
+ return "time(" + timePrecision + ")";
+ case TIMESTAMP:
+ Types.TimestampType timestampType = (Types.TimestampType) type;
+ int timestampPrecision =
+ timestampType.hasPrecisionSet() ? timestampType.precision() :
DEFAULT_PRECISION;
+ return timestampType.hasTimeZone()
+ ? "timestamp(" + timestampPrecision + ") with time zone"
+ : "timestamp(" + timestampPrecision + ")";
+ case UUID:
+ return "uuid";
+ case DECIMAL:
+ Types.DecimalType decimalType = (Types.DecimalType) type;
+ return "decimal(" + decimalType.precision() + "," +
decimalType.scale() + ")";
+ case BINARY:
+ return "varbinary";
+ case LIST:
+ return "array(" + toTrinoTypeString(((Types.ListType)
type).elementType()) + ")";
+ case MAP:
+ Types.MapType mapType = (Types.MapType) type;
+ return "map("
+ + toTrinoTypeString(mapType.keyType())
+ + ","
+ + toTrinoTypeString(mapType.valueType())
+ + ")";
+ case STRUCT:
+ Types.StructType structType = (Types.StructType) type;
+ StringBuilder row = new StringBuilder("row(");
+ Types.StructType.Field[] fields = structType.fields();
+ for (int i = 0; i < fields.length; i++) {
+ if (i > 0) {
+ row.append(',');
+ }
+ row.append(rowFieldName(fields[i].name()))
+ .append(' ')
+ .append(toTrinoTypeString(fields[i].type()));
+ }
+ return row.append(')').toString();
+ default:
+ throw new UnsupportedOperationException("Unsupported conversion to
Trino type: " + type);
+ }
+ }
+
+ /**
+ * Quotes a row field name, matching Trino's own {@code NamedTypeSignature},
which always quotes
+ * named row fields unconditionally; a plain-looking name can still be a
reserved keyword (e.g.
+ * {@code select}), which Trino cannot parse unquoted.
+ */
+ private static String rowFieldName(String name) {
+ return "\"" + name.replace("\"", "\"\"") + "\"";
+ }
+
+ /**
+ * Converts a Trino type signature string (as produced by {@link
#toTrinoTypeString}) back into a
+ * Gravitino type. Used to restore a Trino dialect view's real column list
from its encoded
+ * payload, since the underlying HMS table only carries a single dummy
column (matching Trino's
+ * own native behavior; see {@code
io.trino.plugin.hive.HiveMetadata#createView}).
+ *
+ * @param typeString the Trino type signature string
+ * @return the Gravitino type
+ */
+ static Type fromTrinoTypeString(String typeString) {
+ String s = typeString.trim();
+ String lower = s.toLowerCase(Locale.ROOT);
+ switch (lower) {
+ case "boolean":
+ return Types.BooleanType.get();
+ case "tinyint":
+ return Types.ByteType.get();
+ case "smallint":
+ return Types.ShortType.get();
+ case "integer":
+ return Types.IntegerType.get();
+ case "bigint":
+ return Types.LongType.get();
+ case "real":
+ return Types.FloatType.get();
+ case "double":
+ return Types.DoubleType.get();
+ case "varchar":
+ return Types.StringType.get();
+ case "date":
+ return Types.DateType.get();
+ case "varbinary":
+ return Types.BinaryType.get();
+ case "uuid":
+ return Types.UUIDType.get();
+ default:
+ break;
+ }
+ if (lower.startsWith("varchar(")) {
+ return Types.VarCharType.of(Integer.parseInt(innerContent(s)));
+ }
+ if (lower.startsWith("char(")) {
+ return Types.FixedCharType.of(Integer.parseInt(innerContent(s)));
+ }
+ if (lower.startsWith("time(")) {
+ if (lower.endsWith("with time zone")) {
+ throw new UnsupportedOperationException(
+ "Unsupported Trino type: TIME WITH TIME ZONE has no Gravitino
equivalent: "
+ + typeString);
+ }
+ return Types.TimeType.of(parsePrecision(s));
+ }
+ if (lower.startsWith("timestamp(")) {
+ return lower.endsWith("with time zone")
+ ? Types.TimestampType.withTimeZone(parsePrecision(s))
+ : Types.TimestampType.withoutTimeZone(parsePrecision(s));
+ }
+ if (lower.startsWith("decimal(")) {
+ String[] parts = splitTopLevel(innerContent(s));
+ return Types.DecimalType.of(
+ Integer.parseInt(parts[0].trim()),
Integer.parseInt(parts[1].trim()));
+ }
+ if (lower.startsWith("array(")) {
+ return Types.ListType.nullable(fromTrinoTypeString(innerContent(s)));
+ }
+ if (lower.startsWith("map(")) {
+ String[] parts = splitTopLevel(innerContent(s));
+ return Types.MapType.valueNullable(
+ fromTrinoTypeString(parts[0]), fromTrinoTypeString(parts[1]));
+ }
+ if (lower.startsWith("row(")) {
+ String[] parts = splitTopLevel(innerContent(s));
+ Types.StructType.Field[] fields = new
Types.StructType.Field[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ fields[i] = parseRowField(parts[i].trim(), typeString);
+ }
+ return Types.StructType.of(fields);
+ }
+ throw new UnsupportedOperationException("Unsupported Trino type: " +
typeString);
+ }
+
+ private static String innerContent(String s) {
+ return s.substring(s.indexOf('(') + 1, s.length() - 1);
+ }
+
+ /**
+ * Extracts the integer precision from a type string of the form {@code
name(N)[ trailing text]},
+ * e.g. {@code timestamp(3) with time zone}; unlike {@link #innerContent},
this does not assume
+ * the string ends with the closing parenthesis.
+ */
+ private static int parsePrecision(String s) {
+ int openIdx = s.indexOf('(');
+ int closeIdx = s.indexOf(')', openIdx);
+ return Integer.parseInt(s.substring(openIdx + 1, closeIdx).trim());
+ }
+
+ /**
+ * Parses a single {@code row(...)} field of the form {@code name type} or
{@code "quoted name"
+ * type} into a Gravitino struct field. Gravitino's struct type has no
concept of an anonymous
+ * field, so a Trino row field without a name (e.g. from {@code ROW(1,
'a')}) is rejected rather
+ * than guessed at.
+ */
+ private static Types.StructType.Field parseRowField(String field, String
typeString) {
+ String fieldName;
+ String fieldType;
+ if (field.startsWith("\"")) {
+ int closeQuote = field.indexOf('"', 1);
+ while (closeQuote != -1
+ && closeQuote + 1 < field.length()
+ && field.charAt(closeQuote + 1) == '"') {
+ closeQuote = field.indexOf('"', closeQuote + 2);
+ }
+ if (closeQuote == -1) {
+ throw new UnsupportedOperationException(
+ "Unsupported Trino type: malformed quoted row field name: " +
typeString);
+ }
+ fieldName = field.substring(1, closeQuote).replace("\"\"", "\"");
+ fieldType = field.substring(closeQuote + 1).trim();
+ } else {
+ int spaceIdx = field.indexOf(' ');
+ if (spaceIdx <= 0) {
+ throw new UnsupportedOperationException(
+ "Unsupported Trino type: anonymous row fields are not supported: "
+ typeString);
+ }
+ fieldName = field.substring(0, spaceIdx);
+ fieldType = field.substring(spaceIdx + 1);
+ }
+ return Types.StructType.Field.nullableField(fieldName,
fromTrinoTypeString(fieldType));
+ }
+
+ /**
+ * Splits a comma-separated argument list, ignoring commas nested inside
parentheses or inside a
+ * double-quoted row field name (which may itself contain commas, e.g.
{@code row("a,b" integer)}
+ * ; a doubled {@code ""} inside the quotes is an escaped literal quote, not
a terminator).
+ */
+ private static String[] splitTopLevel(String s) {
+ List<String> parts = new ArrayList<>();
+ int depth = 0;
+ boolean inQuotes = false;
+ int start = 0;
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '"') {
+ if (inQuotes && i + 1 < s.length() && s.charAt(i + 1) == '"') {
+ i++;
+ } else {
+ inQuotes = !inQuotes;
+ }
+ } else if (inQuotes) {
+ // Ignore parentheses/commas inside a quoted field name.
+ } else if (c == '(') {
+ depth++;
+ } else if (c == ')') {
+ depth--;
+ } else if (c == ',' && depth == 0) {
+ parts.add(s.substring(start, i));
+ start = i + 1;
+ }
+ }
+ parts.add(s.substring(start));
+ return parts.toArray(new String[0]);
+ }
+}
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 e0b600fb69..bd651aacbf 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
@@ -49,7 +49,9 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -215,7 +217,7 @@ class TestHiveCatalogOperations {
}
@Test
- void testCreateViewRejectsTrinoDialect() throws Exception {
+ void testCreateViewAcceptsTrinoDialect() throws Exception {
HiveCatalogOperations op = new HiveCatalogOperations();
op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
@@ -223,6 +225,101 @@ class TestHiveCatalogOperations {
HiveClient hiveClient = mock(HiveClient.class);
HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing().when(hiveClient).createTable(hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ View view =
+ op.createView(
+ NameIdentifier.of("db", "v_trino"),
+ null,
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("trino").withSql("SELECT
1").build()
+ },
+ null,
+ null,
+ Maps.newHashMap());
+
+ Assertions.assertNotNull(view);
+ Assertions.assertEquals(1, view.representations().length);
+ SQLRepresentation rep = (SQLRepresentation) view.representations()[0];
+ Assertions.assertEquals("trino", rep.dialect());
+ Assertions.assertEquals("SELECT 1", rep.sql());
+
+ Map<String, String> storedProperties =
hiveTableCaptor.getValue().properties();
+ Assertions.assertEquals("true", storedProperties.get("presto_view"));
+ Assertions.assertEquals("Presto View", storedProperties.get("comment"));
+ TrinoNativeViewCodec.ViewDefinition decoded =
+
TrinoNativeViewCodec.decode(hiveTableCaptor.getValue().viewOriginalText());
+ Assertions.assertEquals("SELECT 1", decoded.originalSql);
+ }
+
+ @Test
+ void testCreateViewLoadRoundTripPreservesTrinoComment() throws Exception {
+ // A Trino dialect view's user-facing comment lives inside the encoded
payload, not the HMS
+ // "comment" property (which is fixed to the "Presto View" marker), so it
must round-trip
+ // through create + load.
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
+ when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing().when(hiveClient).createTable(hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ View created =
+ op.createView(
+ NameIdentifier.of("db", "v_trino"),
+ "a view comment",
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("trino").withSql("SELECT
1").build()
+ },
+ null,
+ null,
+ Maps.newHashMap());
+ Assertions.assertEquals("a view comment", created.comment());
+
+ when(hiveClient.getTable(anyString(), anyString(), anyString()))
+ .thenReturn(hiveTableCaptor.getValue());
+ View loaded = op.loadView(NameIdentifier.of("db", "v_trino"));
+ Assertions.assertEquals("a view comment", loaded.comment());
+ }
+
+ @Test
+ void testCreateViewTrinoDialectStoresDummyHmsColumnAndRealColumnsInPayload()
throws Exception {
+ // Real Trino stores only a single dummy HMS column for a Presto View (see
+ // io.trino.plugin.hive.HiveMetadata#createView); the real columns live in
the encoded
+ // payload. Gravitino must match this so a native Trino reading the HMS
table directly (or
+ // Gravitino reloading its own view) resolves the correct column
count/types.
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
+ when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing().when(hiveClient).createTable(hiveTableCaptor.capture());
when(clientPool.run(any()))
.thenAnswer(
invocation -> {
@@ -231,21 +328,160 @@ class TestHiveCatalogOperations {
});
op.clientPool = clientPool;
- UnsupportedOperationException exception =
+ Column[] columns = {
+ Column.of("id", Types.LongType.get(), "id column"),
+ Column.of("name", Types.StringType.get(), null)
+ };
+
+ View created =
+ op.createView(
+ NameIdentifier.of("db", "v_trino"),
+ null,
+ columns,
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("trino").withSql("SELECT
id, name").build()
+ },
+ null,
+ null,
+ Maps.newHashMap());
+
+ HiveTable storedTable = hiveTableCaptor.getValue();
+ Assertions.assertEquals(1, storedTable.columns().length);
+ Assertions.assertEquals("dummy", storedTable.columns()[0].name());
+
+ Assertions.assertEquals(2, created.columns().length);
+ Assertions.assertEquals("id", created.columns()[0].name());
+ Assertions.assertEquals("name", created.columns()[1].name());
+
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(storedTable);
+ View reloaded = op.loadView(NameIdentifier.of("db", "v_trino"));
+ Assertions.assertEquals(2, reloaded.columns().length);
+ Assertions.assertEquals("id", reloaded.columns()[0].name());
+ Assertions.assertEquals("id column", reloaded.columns()[0].comment());
+ Assertions.assertEquals("name", reloaded.columns()[1].name());
+ }
+
+ @Test
+ void testCreateViewPersistsTrinoDefaultCatalogAndSchema() throws Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
+ when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing().when(hiveClient).createTable(hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ // Simulates a Trino "USE gt_hive.db; CREATE VIEW v AS SELECT * FROM t"
statement, where
+ // the unqualified "t" must be resolved against
defaultCatalog/defaultSchema on reload.
+ op.createView(
+ NameIdentifier.of("db", "v_trino"),
+ null,
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("trino").withSql("SELECT *
FROM t").build()
+ },
+ "gt_hive",
+ "db",
+ Maps.newHashMap());
+
+ TrinoNativeViewCodec.ViewDefinition decoded =
+
TrinoNativeViewCodec.decode(hiveTableCaptor.getValue().viewOriginalText());
+ Assertions.assertEquals("gt_hive", decoded.catalog);
+ Assertions.assertEquals("db", decoded.schema);
+
+ when(hiveClient.getTable(anyString(), anyString(), anyString()))
+ .thenReturn(hiveTableCaptor.getValue());
+ View loaded = op.loadView(NameIdentifier.of("db", "v_trino"));
+
+ Assertions.assertEquals("gt_hive", loaded.defaultCatalog());
+ Assertions.assertEquals("db", loaded.defaultSchema());
+ }
+
+ @Test
+ void testCreateViewRejectsTrinoSchemaWithoutCatalog() throws Exception {
+ // Trino's own ConnectorViewDefinition rejects a schema without a catalog;
accepting it here
+ // would persist a payload that a native Trino connector cannot decode.
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
+ when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ IllegalArgumentException exception =
Assertions.assertThrows(
- UnsupportedOperationException.class,
+ IllegalArgumentException.class,
() ->
op.createView(
NameIdentifier.of("db", "v_trino"),
null,
- new Column[0],
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
new SQLRepresentation[] {
SQLRepresentation.builder().withDialect("trino").withSql("SELECT 1").build()
},
null,
- null,
+ "db",
Maps.newHashMap()));
- Assertions.assertTrue(exception.getMessage().contains("supports only"));
+
+ Assertions.assertTrue(
+ exception
+ .getMessage()
+ .contains("does not support a defaultSchema without a
defaultCatalog"));
+ }
+
+ @Test
+ void testCreateViewClearsStaleTrinoMarkerForNonTrinoDialect() throws
Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveSchema schema =
HiveSchema.builder().withCatalogName("hive").withName("db").build();
+ when(hiveClient.getDatabase(anyString(), anyString())).thenReturn(schema);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing().when(hiveClient).createTable(hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ Map<String, String> propertiesWithStaleMarker = Maps.newHashMap();
+ propertiesWithStaleMarker.put("presto_view", "true");
+
+ op.createView(
+ NameIdentifier.of("db", "v_hive"),
+ null,
+ new Column[0],
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("hive").withSql("SELECT
1").build()
+ },
+ null,
+ null,
+ propertiesWithStaleMarker);
+
+
Assertions.assertNull(hiveTableCaptor.getValue().properties().get("presto_view"));
}
@Test
@@ -481,12 +717,23 @@ class TestHiveCatalogOperations {
}
@Test
- void testLoadViewRejectsTrinoDialect() throws Exception {
+ void testLoadViewAcceptsTrinoDialect() throws Exception {
HiveCatalogOperations op = new HiveCatalogOperations();
op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
CachedClientPool clientPool = mock(CachedClientPool.class);
HiveClient hiveClient = mock(HiveClient.class);
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("_col0",
"integer", null)),
+ null,
+ null,
+ true,
+ List.of()));
when(hiveClient.getTable(anyString(), anyString(), anyString()))
.thenReturn(
HiveTable.builder()
@@ -494,6 +741,7 @@ class TestHiveCatalogOperations {
.withCatalogName("hive")
.withDatabaseName("db")
.withColumns(new Column[0])
+ .withComment("Presto View")
.withProperties(
Maps.newHashMap(
ImmutableMap.of(
@@ -501,7 +749,7 @@ class TestHiveCatalogOperations {
TableType.VIRTUAL_VIEW.name(),
"presto_view",
"true")))
- .withViewOriginalText("SELECT 1")
+ .withViewOriginalText(encoded)
.build());
when(clientPool.run(any()))
.thenAnswer(
@@ -511,11 +759,107 @@ class TestHiveCatalogOperations {
});
op.clientPool = clientPool;
- UnsupportedOperationException exception =
- Assertions.assertThrows(
- UnsupportedOperationException.class,
- () -> op.loadView(NameIdentifier.of("db", "v_trino")));
- Assertions.assertTrue(exception.getMessage().contains("supports only"));
+ View loaded = op.loadView(NameIdentifier.of("db", "v_trino"));
+
+ SQLRepresentation representation = (SQLRepresentation)
loaded.representations()[0];
+ Assertions.assertEquals("trino", representation.dialect());
+ Assertions.assertEquals("SELECT 1", representation.sql());
+ }
+
+ @Test
+ void testLoadViewAcceptsNativeTrinoView() throws Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ // A native Presto/Trino view (created outside Gravitino, e.g. by a native
Trino Hive
+ // connector pointed at the same Hive Metastore) is encoded using Trino's
own native format;
+ // Gravitino must be able to read it directly, not just views it created
itself.
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ "native_catalog",
+ "native_db",
+ List.of(new TrinoNativeViewCodec.ViewColumn("id", "integer",
null)),
+ "a comment",
+ null,
+ true,
+ List.of()));
+ when(hiveClient.getTable(anyString(), anyString(), anyString()))
+ .thenReturn(
+ HiveTable.builder()
+ .withName("v_native_trino")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ View loaded = op.loadView(NameIdentifier.of("db", "v_native_trino"));
+
+ SQLRepresentation representation = (SQLRepresentation)
loaded.representations()[0];
+ Assertions.assertEquals("trino", representation.dialect());
+ Assertions.assertEquals("SELECT 1", representation.sql());
+ Assertions.assertEquals("a comment", loaded.comment());
+ Assertions.assertEquals("native_catalog", loaded.defaultCatalog());
+ Assertions.assertEquals("native_db", loaded.defaultSchema());
+ }
+
+ @Test
+ void testLoadViewRejectsPrestoViewThatIsNotAPlainView() throws Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ // A Trino materialized view also carries presto_view=true, but with a
different comment
+ // marker ("Presto Materialized View"); it must be rejected rather than
misread as a plain
+ // Trino view.
+ when(hiveClient.getTable(anyString(), anyString(), anyString()))
+ .thenReturn(
+ HiveTable.builder()
+ .withName("v_materialized")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto Materialized View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText("/* Presto View: base64encodedpayload
*/")
+ .build());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> op.loadView(NameIdentifier.of("db", "v_materialized")));
}
@Test
@@ -590,7 +934,291 @@ class TestHiveCatalogOperations {
}
@Test
- void testAlterViewReplaceRejectsTrinoDialect() throws Exception {
+ void testAlterViewReplaceAcceptsTrinoDialect() throws Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ null,
+ true,
+ List.of()));
+ HiveTable currentTable =
+ HiveTable.builder()
+ .withName("v_hive")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build();
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(currentTable);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing()
+ .when(hiveClient)
+ .alterTable(anyString(), anyString(), anyString(),
hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ View updated =
+ op.alterView(
+ NameIdentifier.of("db", "v_hive"),
+ ViewChange.replaceView(
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
+ new SQLRepresentation[] {
+
SQLRepresentation.builder().withDialect("trino").withSql("SELECT 2").build()
+ },
+ null,
+ null,
+ null));
+
+ Assertions.assertEquals("true",
hiveTableCaptor.getValue().properties().get("presto_view"));
+ Assertions.assertEquals("Presto View",
hiveTableCaptor.getValue().properties().get("comment"));
+ TrinoNativeViewCodec.ViewDefinition decoded =
+
TrinoNativeViewCodec.decode(hiveTableCaptor.getValue().viewOriginalText());
+ Assertions.assertEquals("SELECT 2", decoded.originalSql);
+ SQLRepresentation representation = (SQLRepresentation)
updated.representations()[0];
+ Assertions.assertEquals("trino", representation.dialect());
+ Assertions.assertEquals("SELECT 2", representation.sql());
+ }
+
+ @Test
+ void testAlterViewReplaceRejectsExistingNonDefaultOwner() throws Exception {
+ // Gravitino's view model has no owner concept; replacing a native Trino
view that has a
+ // non-null owner (a SECURITY DEFINER view) would silently turn it into an
ownerless view.
+ assertReplaceRejectsUnrepresentableNativeView(
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ "alice",
+ true,
+ List.of())));
+ }
+
+ @Test
+ void testAlterViewReplaceRejectsExistingRunAsInvokerFalse() throws Exception
{
+ // runAsInvoker=false means SECURITY DEFINER; Gravitino always writes
runAsInvoker=true, so
+ // replacing such a view would silently downgrade it to SECURITY INVOKER.
+ assertReplaceRejectsUnrepresentableNativeView(
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ null,
+ false,
+ List.of())));
+ }
+
+ @Test
+ void testAlterViewReplaceRejectsExistingNonEmptyPath() throws Exception {
+ // Gravitino's view model has no SQL path concept; replacing a native
Trino view that has a
+ // non-empty path would silently discard it. TrinoNativeViewCodec.encode()
always writes an
+ // empty path (Gravitino itself never produces one), so this payload is
built by hand to
+ // simulate a view created directly by a native Trino connector with a
non-empty path.
+ String encoded =
+ "/* Presto View: "
+ + Base64.getEncoder()
+ .encodeToString(
+ ("{\"originalSql\":\"SELECT
1\",\"catalog\":null,\"schema\":null,"
+ +
"\"columns\":[{\"name\":\"c1\",\"type\":\"integer\",\"comment\":null}],"
+ +
"\"comment\":null,\"owner\":null,\"runAsInvoker\":true,"
+ +
"\"path\":[{\"catalog\":\"c\",\"schema\":\"s\"}]}")
+ .getBytes(StandardCharsets.UTF_8))
+ + " */";
+ assertReplaceRejectsUnrepresentableNativeView(encoded);
+ }
+
+ private void assertReplaceRejectsUnrepresentableNativeView(String encoded)
throws Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ HiveTable currentTable =
+ HiveTable.builder()
+ .withName("v_trino")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build();
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(currentTable);
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ op.alterView(
+ NameIdentifier.of("db", "v_trino"),
+ ViewChange.replaceView(
+ new Column[] {Column.of("c1", Types.IntegerType.get())},
+ new SQLRepresentation[] {
+
SQLRepresentation.builder().withDialect("trino").withSql("SELECT 2").build()
+ },
+ null,
+ null,
+ null)));
+ }
+
+ @Test
+ void testAlterViewRejectsSetPropertyCommentOnTrinoView() throws Exception {
+ // A Trino dialect view's HMS "comment" property is fixed to "Presto View"
(the marker Trino
+ // itself relies on to recognize the view); the real comment lives inside
the encoded payload.
+ // Setting it directly (bypassing ReplaceView) would desynchronize the two
and make the view
+ // unloadable on the next read, so it must be rejected instead.
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ null,
+ true,
+ List.of()));
+ HiveTable currentTable =
+ HiveTable.builder()
+ .withName("v_trino")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build();
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(currentTable);
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ op.alterView(
+ NameIdentifier.of("db", "v_trino"),
ViewChange.setProperty("comment", "my note")));
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ op.alterView(NameIdentifier.of("db", "v_trino"),
ViewChange.removeProperty("comment")));
+ }
+
+ @Test
+ void testAlterViewRejectsRemovingPrestoViewMarkerFromTrinoView() throws
Exception {
+ // The presto_view HMS property is part of the native Trino view storage
contract; removing it
+ // directly (bypassing ReplaceView) would leave the encoded payload in
viewOriginalText while
+ // making the view misclassify as Hive dialect on the next load, exposing
the raw payload as
+ // SQL.
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ null,
+ true,
+ List.of()));
+ HiveTable currentTable =
+ HiveTable.builder()
+ .withName("v_trino")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build();
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(currentTable);
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ op.alterView(
+ NameIdentifier.of("db", "v_trino"),
ViewChange.removeProperty("presto_view")));
+ }
+
+ @Test
+ void testAlterViewRejectsSettingPrestoViewMarkerOnNonTrinoView() throws
Exception {
+ // Setting presto_view=true on a plain Hive view directly (bypassing
ReplaceView) would make
+ // the view misclassify as Trino dialect on the next load, and decoding
its plain SQL
+ // viewOriginalText as a Trino native payload would fail.
HiveCatalogOperations op = new HiveCatalogOperations();
op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
@@ -616,24 +1244,73 @@ class TestHiveCatalogOperations {
});
op.clientPool = clientPool;
- UnsupportedOperationException exception =
- Assertions.assertThrows(
- UnsupportedOperationException.class,
- () ->
- op.alterView(
- NameIdentifier.of("db", "v_hive"),
- ViewChange.replaceView(
- new Column[0],
- new SQLRepresentation[] {
- SQLRepresentation.builder()
- .withDialect("trino")
- .withSql("SELECT 2")
- .build()
- },
- null,
- null,
- null)));
- Assertions.assertTrue(exception.getMessage().contains("supports only"));
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ op.alterView(
+ NameIdentifier.of("db", "v_hive"),
ViewChange.setProperty("presto_view", "true")));
+ }
+
+ @Test
+ void testAlterViewReplaceClearsTrinoMarkerForNonTrinoDialect() throws
Exception {
+ HiveCatalogOperations op = new HiveCatalogOperations();
+ op.initialize(Maps.newHashMap(), null, HIVE_PROPERTIES_METADATA);
+
+ CachedClientPool clientPool = mock(CachedClientPool.class);
+ HiveClient hiveClient = mock(HiveClient.class);
+ String encoded =
+ TrinoNativeViewCodec.encode(
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("c1", "integer",
null)),
+ null,
+ null,
+ true,
+ List.of()));
+ HiveTable currentTable =
+ HiveTable.builder()
+ .withName("v_trino")
+ .withCatalogName("hive")
+ .withDatabaseName("db")
+ .withColumns(new Column[0])
+ .withComment("Presto View")
+ .withProperties(
+ Maps.newHashMap(
+ ImmutableMap.of(
+ HiveConstants.TABLE_TYPE,
+ TableType.VIRTUAL_VIEW.name(),
+ "presto_view",
+ "true")))
+ .withViewOriginalText(encoded)
+ .build();
+ when(hiveClient.getTable(anyString(), anyString(),
anyString())).thenReturn(currentTable);
+
+ ArgumentCaptor<HiveTable> hiveTableCaptor =
ArgumentCaptor.forClass(HiveTable.class);
+ doNothing()
+ .when(hiveClient)
+ .alterTable(anyString(), anyString(), anyString(),
hiveTableCaptor.capture());
+ when(clientPool.run(any()))
+ .thenAnswer(
+ invocation -> {
+ ClientPool.Action<?, HiveClient, ?> action =
invocation.getArgument(0);
+ return action.run(hiveClient);
+ });
+ op.clientPool = clientPool;
+
+ op.alterView(
+ NameIdentifier.of("db", "v_trino"),
+ ViewChange.replaceView(
+ new Column[0],
+ new SQLRepresentation[] {
+ SQLRepresentation.builder().withDialect("hive").withSql("SELECT
2").build()
+ },
+ null,
+ null,
+ null));
+
+
Assertions.assertNull(hiveTableCaptor.getValue().properties().get("presto_view"));
}
@Test
diff --git
a/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestTrinoNativeViewCodec.java
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestTrinoNativeViewCodec.java
new file mode 100644
index 0000000000..b936896650
--- /dev/null
+++
b/catalogs/catalog-hive/src/test/java/org/apache/gravitino/catalog/hive/TestTrinoNativeViewCodec.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog.hive;
+
+import java.util.List;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestTrinoNativeViewCodec {
+
+ @Test
+ void testEncodeDecodeRoundTrip() {
+ TrinoNativeViewCodec.ViewDefinition definition =
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT id, name FROM t",
+ "hive",
+ "db",
+ List.of(
+ new TrinoNativeViewCodec.ViewColumn("id", "bigint", null),
+ new TrinoNativeViewCodec.ViewColumn("name", "varchar(50)",
"name column")),
+ "a view comment",
+ null,
+ true,
+ List.of());
+
+ String encoded = TrinoNativeViewCodec.encode(definition);
+ Assertions.assertTrue(encoded.startsWith("/* Presto View: "));
+ Assertions.assertTrue(encoded.endsWith(" */"));
+
+ TrinoNativeViewCodec.ViewDefinition decoded =
TrinoNativeViewCodec.decode(encoded);
+ Assertions.assertEquals("SELECT id, name FROM t", decoded.originalSql);
+ Assertions.assertEquals("hive", decoded.catalog);
+ Assertions.assertEquals("db", decoded.schema);
+ Assertions.assertEquals("a view comment", decoded.comment);
+ Assertions.assertNull(decoded.owner);
+ Assertions.assertTrue(decoded.runAsInvoker);
+ Assertions.assertEquals(2, decoded.columns.size());
+ Assertions.assertEquals("id", decoded.columns.get(0).name);
+ Assertions.assertEquals("bigint", decoded.columns.get(0).type);
+ Assertions.assertNull(decoded.columns.get(0).comment);
+ Assertions.assertEquals("name", decoded.columns.get(1).name);
+ Assertions.assertEquals("varchar(50)", decoded.columns.get(1).type);
+ Assertions.assertEquals("name column", decoded.columns.get(1).comment);
+ }
+
+ @Test
+ void testEncodeDecodeRoundTripWithNullCatalogAndSchema() {
+ TrinoNativeViewCodec.ViewDefinition definition =
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1",
+ null,
+ null,
+ List.of(new TrinoNativeViewCodec.ViewColumn("_col0", "integer",
null)),
+ null,
+ null,
+ true,
+ List.of());
+
+ TrinoNativeViewCodec.ViewDefinition decoded =
+ TrinoNativeViewCodec.decode(TrinoNativeViewCodec.encode(definition));
+ Assertions.assertNull(decoded.catalog);
+ Assertions.assertNull(decoded.schema);
+ Assertions.assertNull(decoded.comment);
+ }
+
+ @Test
+ void testDecodeRejectsMissingPrefixOrSuffix() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> TrinoNativeViewCodec.decode("not
a presto view"));
+ }
+
+ @Test
+ void testToTrinoTypeStringForPrimitives() {
+ Assertions.assertEquals(
+ "boolean",
TrinoNativeViewCodec.toTrinoTypeString(Types.BooleanType.get()));
+ Assertions.assertEquals(
+ "tinyint",
TrinoNativeViewCodec.toTrinoTypeString(Types.ByteType.get()));
+ Assertions.assertEquals(
+ "smallint",
TrinoNativeViewCodec.toTrinoTypeString(Types.ShortType.get()));
+ Assertions.assertEquals(
+ "integer",
TrinoNativeViewCodec.toTrinoTypeString(Types.IntegerType.get()));
+ Assertions.assertEquals("bigint",
TrinoNativeViewCodec.toTrinoTypeString(Types.LongType.get()));
+ Assertions.assertEquals("real",
TrinoNativeViewCodec.toTrinoTypeString(Types.FloatType.get()));
+ Assertions.assertEquals(
+ "double",
TrinoNativeViewCodec.toTrinoTypeString(Types.DoubleType.get()));
+ Assertions.assertEquals(
+ "varchar",
TrinoNativeViewCodec.toTrinoTypeString(Types.StringType.get()));
+ Assertions.assertEquals(
+ "varchar(10)",
TrinoNativeViewCodec.toTrinoTypeString(Types.VarCharType.of(10)));
+ Assertions.assertEquals(
+ "char(5)",
TrinoNativeViewCodec.toTrinoTypeString(Types.FixedCharType.of(5)));
+ Assertions.assertEquals("date",
TrinoNativeViewCodec.toTrinoTypeString(Types.DateType.get()));
+ Assertions.assertEquals(
+ "timestamp(3)",
+
TrinoNativeViewCodec.toTrinoTypeString(Types.TimestampType.withoutTimeZone()));
+ Assertions.assertEquals(
+ "timestamp(3) with time zone",
+
TrinoNativeViewCodec.toTrinoTypeString(Types.TimestampType.withTimeZone()));
+ Assertions.assertEquals(
+ "time(3)",
TrinoNativeViewCodec.toTrinoTypeString(Types.TimeType.get()));
+ Assertions.assertEquals("uuid",
TrinoNativeViewCodec.toTrinoTypeString(Types.UUIDType.get()));
+ Assertions.assertEquals(
+ "decimal(10,2)",
TrinoNativeViewCodec.toTrinoTypeString(Types.DecimalType.of(10, 2)));
+ Assertions.assertEquals(
+ "varbinary",
TrinoNativeViewCodec.toTrinoTypeString(Types.BinaryType.get()));
+ }
+
+ @Test
+ void testToTrinoTypeStringForComplexTypes() {
+ Assertions.assertEquals(
+ "array(integer)",
+
TrinoNativeViewCodec.toTrinoTypeString(Types.ListType.nullable(Types.IntegerType.get())));
+ Assertions.assertEquals(
+ "map(varchar,integer)",
+ TrinoNativeViewCodec.toTrinoTypeString(
+ Types.MapType.valueNullable(Types.StringType.get(),
Types.IntegerType.get())));
+ Assertions.assertEquals(
+ "row(\"a\" integer,\"b\" varchar)",
+ TrinoNativeViewCodec.toTrinoTypeString(
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("a",
Types.IntegerType.get()),
+ Types.StructType.Field.nullableField("b",
Types.StringType.get()))));
+ }
+
+ @Test
+ void testToTrinoTypeStringPreservesExplicitPrecision() {
+ Assertions.assertEquals(
+ "timestamp(6)",
+
TrinoNativeViewCodec.toTrinoTypeString(Types.TimestampType.withoutTimeZone(6)));
+ Assertions.assertEquals(
+ "timestamp(6) with time zone",
+
TrinoNativeViewCodec.toTrinoTypeString(Types.TimestampType.withTimeZone(6)));
+ Assertions.assertEquals(
+ "time(6)",
TrinoNativeViewCodec.toTrinoTypeString(Types.TimeType.of(6)));
+ }
+
+ @Test
+ void testToTrinoTypeStringWidensUnsignedIntegralTypes() {
+ Assertions.assertEquals(
+ "smallint",
TrinoNativeViewCodec.toTrinoTypeString(Types.ByteType.unsigned()));
+ Assertions.assertEquals(
+ "integer",
TrinoNativeViewCodec.toTrinoTypeString(Types.ShortType.unsigned()));
+ Assertions.assertEquals(
+ "bigint",
TrinoNativeViewCodec.toTrinoTypeString(Types.IntegerType.unsigned()));
+ Assertions.assertEquals(
+ "decimal(20,0)",
TrinoNativeViewCodec.toTrinoTypeString(Types.LongType.unsigned()));
+ }
+
+ @Test
+ void testFromTrinoTypeStringForPrimitives() {
+ Assertions.assertEquals(
+ Types.BooleanType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("boolean"));
+ Assertions.assertEquals(
+ Types.ByteType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("tinyint"));
+ Assertions.assertEquals(
+ Types.ShortType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("smallint"));
+ Assertions.assertEquals(
+ Types.IntegerType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("integer"));
+ Assertions.assertEquals(
+ Types.LongType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("bigint"));
+ Assertions.assertEquals(
+ Types.FloatType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("real"));
+ Assertions.assertEquals(
+ Types.DoubleType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("double"));
+ Assertions.assertEquals(
+ Types.StringType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("varchar"));
+ Assertions.assertEquals(
+ Types.VarCharType.of(10),
TrinoNativeViewCodec.fromTrinoTypeString("varchar(10)"));
+ Assertions.assertEquals(
+ Types.FixedCharType.of(5),
TrinoNativeViewCodec.fromTrinoTypeString("char(5)"));
+ Assertions.assertEquals(Types.DateType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("date"));
+ Assertions.assertEquals(
+ Types.TimestampType.withoutTimeZone(6),
+ TrinoNativeViewCodec.fromTrinoTypeString("timestamp(6)"));
+ Assertions.assertEquals(
+ Types.TimestampType.withTimeZone(6),
+ TrinoNativeViewCodec.fromTrinoTypeString("timestamp(6) with time
zone"));
+ Assertions.assertEquals(
+ Types.TimeType.of(6),
TrinoNativeViewCodec.fromTrinoTypeString("time(6)"));
+ Assertions.assertEquals(Types.UUIDType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("uuid"));
+ Assertions.assertEquals(
+ Types.DecimalType.of(10, 2),
TrinoNativeViewCodec.fromTrinoTypeString("decimal(10,2)"));
+ Assertions.assertEquals(
+ Types.BinaryType.get(),
TrinoNativeViewCodec.fromTrinoTypeString("varbinary"));
+ }
+
+ @Test
+ void testFromTrinoTypeStringForComplexTypesRoundTripsWithToTrinoTypeString()
{
+ Types.ListType listType = Types.ListType.nullable(Types.IntegerType.get());
+ Assertions.assertEquals(
+ listType,
+
TrinoNativeViewCodec.fromTrinoTypeString(TrinoNativeViewCodec.toTrinoTypeString(listType)));
+
+ Types.MapType mapType =
+ Types.MapType.valueNullable(Types.StringType.get(),
Types.IntegerType.get());
+ Assertions.assertEquals(
+ mapType,
+
TrinoNativeViewCodec.fromTrinoTypeString(TrinoNativeViewCodec.toTrinoTypeString(mapType)));
+
+ Types.StructType structType =
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("a", Types.IntegerType.get()),
+ Types.StructType.Field.nullableField("b", Types.StringType.get()));
+ Assertions.assertEquals(
+ structType,
+ TrinoNativeViewCodec.fromTrinoTypeString(
+ TrinoNativeViewCodec.toTrinoTypeString(structType)));
+
+ // Nested: array(row(a integer))
+ Types.ListType nested =
+ Types.ListType.nullable(
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("a",
Types.IntegerType.get())));
+ Assertions.assertEquals(
+ nested,
+
TrinoNativeViewCodec.fromTrinoTypeString(TrinoNativeViewCodec.toTrinoTypeString(nested)));
+ }
+
+ @Test
+ void testFromTrinoTypeStringHandlesQuotedRowFieldNames() {
+ Types.StructType structType =
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("my field",
Types.IntegerType.get()));
+ String encoded = TrinoNativeViewCodec.toTrinoTypeString(structType);
+ Assertions.assertEquals("row(\"my field\" integer)", encoded);
+ Assertions.assertEquals(structType,
TrinoNativeViewCodec.fromTrinoTypeString(encoded));
+ }
+
+ @Test
+ void testToTrinoTypeStringQuotesReservedKeywordRowFieldName() {
+ Types.StructType structType =
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("select",
Types.IntegerType.get()));
+ String encoded = TrinoNativeViewCodec.toTrinoTypeString(structType);
+ Assertions.assertEquals("row(\"select\" integer)", encoded);
+ Assertions.assertEquals(structType,
TrinoNativeViewCodec.fromTrinoTypeString(encoded));
+ }
+
+ @Test
+ void testFromTrinoTypeStringRejectsAnonymousRowField() {
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () ->
TrinoNativeViewCodec.fromTrinoTypeString("row(integer,varchar)"));
+ }
+
+ @Test
+ void testTimestampTimeAndUuidRoundTripWithToTrinoTypeString() {
+ Types.TimestampType timestamp = Types.TimestampType.withoutTimeZone(3);
+ Assertions.assertEquals(
+ timestamp,
+ TrinoNativeViewCodec.fromTrinoTypeString(
+ TrinoNativeViewCodec.toTrinoTypeString(timestamp)));
+
+ Types.TimestampType timestampWithTimeZone =
Types.TimestampType.withTimeZone(3);
+ Assertions.assertEquals(
+ timestampWithTimeZone,
+ TrinoNativeViewCodec.fromTrinoTypeString(
+ TrinoNativeViewCodec.toTrinoTypeString(timestampWithTimeZone)));
+
+ Types.TimeType time = Types.TimeType.of(3);
+ Assertions.assertEquals(
+ time,
+
TrinoNativeViewCodec.fromTrinoTypeString(TrinoNativeViewCodec.toTrinoTypeString(time)));
+
+ Types.UUIDType uuid = Types.UUIDType.get();
+ Assertions.assertEquals(
+ uuid,
+
TrinoNativeViewCodec.fromTrinoTypeString(TrinoNativeViewCodec.toTrinoTypeString(uuid)));
+ }
+
+ @Test
+ void testFromTrinoTypeStringRejectsUnknownType() {
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> TrinoNativeViewCodec.fromTrinoTypeString("json"));
+ }
+
+ @Test
+ void testFromTrinoTypeStringRejectsTimeWithTimeZone() {
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> TrinoNativeViewCodec.fromTrinoTypeString("time(3) with time
zone"));
+ }
+
+ @Test
+ void testDecodeRejectsEmptyColumns() {
+ TrinoNativeViewCodec.ViewDefinition definition =
+ new TrinoNativeViewCodec.ViewDefinition(
+ "SELECT 1", null, null, List.of(), null, null, true, List.of());
+ String encoded = TrinoNativeViewCodec.encode(definition);
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () ->
TrinoNativeViewCodec.decode(encoded));
+ }
+
+ @Test
+ void testDecodeRejectsMalformedBase64Payload() {
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> TrinoNativeViewCodec.decode("/* Presto View: not-valid-base64!!
*/"));
+ }
+}
diff --git a/docs/apache-hive-catalog.md b/docs/apache-hive-catalog.md
index 59763b05a1..67f6fcc584 100644
--- a/docs/apache-hive-catalog.md
+++ b/docs/apache-hive-catalog.md
@@ -241,9 +241,11 @@ Support for altering partitions is under development.
- Supports list, create, load, alter, and drop for views stored in the Hive
Metastore Service as `VIRTUAL_VIEW`.
- Each view must contain exactly one SQL representation.
-- Supports creating views with the `hive`, `flink`, or `spark` dialect.
-- When loading an existing HMS view, Gravitino automatically detects whether
the view uses the `hive`, `flink`, `spark`, or `trino` dialect.
+- Supports creating views with the `hive`, `trino`, `flink`, or `spark`
dialect.
+- When loading an existing HMS view, Gravitino automatically detects whether
the view uses the `hive`, `trino`, `flink`, or `spark` dialect.
- For the `hive` and `flink` dialects, `defaultCatalog` and `defaultSchema`
must be `null`.
+- For the `trino` dialect, `defaultSchema` requires `defaultCatalog` to also
be set (a schema without a catalog cannot be represented).
+- The `trino` dialect requires at least one output column, and is stored using
Trino's own native "Presto View" Hive Metastore encoding, so a view created
through Gravitino is interoperable with a native Trino/Presto Hive connector
pointed at the same Hive Metastore, and vice versa. The HMS `presto_view`
property this relies on is reserved and managed internally based on the view's
dialect; it cannot be set or removed directly. Gravitino's view model cannot
represent a native Trino view' [...]
- The `flink` dialect requires at least one view property with the prefix
`flink.` to be set. The Flink connector automatically sets
`flink.schema.num-columns`; when using the REST API directly, set at least one
`flink.*` property explicitly.
- The `spark` dialect requires the view property `spark.sql.create.version` to
be set; without it the view round-trips as the `hive` dialect on reload.