mchades commented on code in PR #12294: URL: https://github.com/apache/gravitino/pull/12294#discussion_r3780458079
########## 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's owner, `runAsInvoker`, or SQL path, so replacing an existing native view that has a non-default value for any of them is rejected rather than silently discarding it. Review Comment: Thanks. As discussed offline, the complete bidirectional interoperability IT will be added in a follow-up PR. ########## catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java: ########## @@ -0,0 +1,454 @@ +/* + * 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 java.util.regex.Pattern; +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(); + private static final Pattern SIMPLE_IDENTIFIER = Pattern.compile("[a-z_][a-z0-9_]*"); + // 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: Review Comment: These mappings ignore `IntegralType.signed()`, including inside nested types. For example, `long unsigned` is persisted as `bigint`, while the existing Trino transformer uses `decimal(20,0)` to preserve its range; reloading also turns it into a signed `long`. Could we reject unsigned view columns or define and test the same widening contract rather than silently narrowing them? ########## catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java: ########## @@ -0,0 +1,454 @@ +/* + * 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 java.util.regex.Pattern; +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(); + private static final Pattern SIMPLE_IDENTIFIER = Pattern.compile("[a-z_][a-z0-9_]*"); + // 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: + return "tinyint"; + case SHORT: + return "smallint"; + case INTEGER: + return "integer"; + case LONG: + return "bigint"; + 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 if it is not a plain lowercase identifier Trino can parse bare. */ + private static String rowFieldName(String name) { + if (SIMPLE_IDENTIFIER.matcher(name).matches()) { + return 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(")) { Review Comment: `time(p) with time zone` also matches this branch and is silently converted to an unzoned `TimeType`. Trino 435 serializes `TIME WITH TIME ZONE` in exactly this form, while Gravitino has no zone-bearing `TimeType`. Could we explicitly reject it and add a regression test instead of changing the column semantics? ########## catalogs/catalog-hive/src/main/java/org/apache/gravitino/catalog/hive/TrinoNativeViewCodec.java: ########## @@ -0,0 +1,454 @@ +/* + * 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 java.util.regex.Pattern; +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(); + private static final Pattern SIMPLE_IDENTIFIER = Pattern.compile("[a-z_][a-z0-9_]*"); + // 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: + return "tinyint"; + case SHORT: + return "smallint"; + case INTEGER: + return "integer"; + case LONG: + return "bigint"; + 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 if it is not a plain lowercase identifier Trino can parse bare. */ + private static String rowFieldName(String name) { + if (SIMPLE_IDENTIFIER.matcher(name).matches()) { Review Comment: `SIMPLE_IDENTIFIER` also accepts reserved words. For example, a Gravitino struct field named `select` is emitted as `row(select integer)`, which Trino cannot parse; Trino's own `NamedTypeSignature` quotes every named row field. Could we quote all field names and add a reserved-keyword test? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
