diff --git a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDatabaseMetaData.java b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDatabaseMetaData.java index 160c2be110ca..f89570d41567 100644 --- a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDatabaseMetaData.java +++ b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestTrinoDatabaseMetaData.java @@ -1192,7 +1192,7 @@ public void testGetTablesMetadataCalls() list(list(COUNTING_CATALOG, "test_schema1", "test_table1", "TABLE")), ImmutableMultiset.builder() .addCopies("ConnectorMetadata.getSystemTable(schema=test_schema1, table=test_table1)", 4) - .add("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.getTableHandle(schema=test_schema1, table=test_table1)") @@ -1394,12 +1394,14 @@ public void testGetColumnsMetadataCalls() .add("ConnectorMetadata.listTables(schema=test_schema4_empty)") .addCopies("ConnectorMetadata.getSystemTable(schema=test_schema1, table=test_table1)", 20) .addCopies("ConnectorMetadata.getMaterializedView(schema=test_schema1, table=test_table1)", 5) - .addCopies("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)", 5) + .addCopies("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)", 1) + .addCopies("ConnectorMetadata.isView(schema=test_schema1, table=test_table1)", 4) .addCopies("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table1)", 5) .addCopies("ConnectorMetadata.getTableHandle(schema=test_schema1, table=test_table1)", 5) .addCopies("ConnectorMetadata.getSystemTable(schema=test_schema2, table=test_table1)", 20) .addCopies("ConnectorMetadata.getMaterializedView(schema=test_schema2, table=test_table1)", 5) - .addCopies("ConnectorMetadata.getView(schema=test_schema2, table=test_table1)", 5) + .addCopies("ConnectorMetadata.getView(schema=test_schema2, table=test_table1)", 1) + .addCopies("ConnectorMetadata.isView(schema=test_schema2, table=test_table1)", 4) .addCopies("ConnectorMetadata.redirectTable(schema=test_schema2, table=test_table1)", 5) .addCopies("ConnectorMetadata.getTableHandle(schema=test_schema2, table=test_table1)", 5) .add("ConnectorMetadata.getTableMetadata(handle=test_schema1.test_table1)") @@ -1535,7 +1537,7 @@ private void testAssumeLiteralMetadataCalls(String escapeLiteralParameter) list(list(COUNTING_CATALOG, "test_schema1", "test_table1", "TABLE")), ImmutableMultiset.builder() .addCopies("ConnectorMetadata.getSystemTable(schema=test_schema1, table=test_table1)", 4) - .add("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.getTableHandle(schema=test_schema1, table=test_table1)") diff --git a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java index 21cb75949731..faea890bfe15 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/Metadata.java +++ b/core/trino-main/src/main/java/io/trino/metadata/Metadata.java @@ -479,13 +479,7 @@ Optional finishRefreshMaterializedView( */ Map getViews(Session session, QualifiedTablePrefix prefix); - /** - * Is the specified table a view. - */ - default boolean isView(Session session, QualifiedObjectName viewName) - { - return getView(session, viewName).isPresent(); - } + boolean isView(Session session, QualifiedObjectName viewName); /** * Returns the view definition for the specified view name. diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java index 1a4a3e04faef..c4d0fc53e3f0 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataManager.java @@ -1476,7 +1476,18 @@ public Optional getSchemaOwner(Session session, CatalogSchemaNam @Override public boolean isView(Session session, QualifiedObjectName viewName) { - return getViewInternal(session, viewName).isPresent(); + if (cannotExist(viewName)) { + return false; + } + + return getOptionalCatalogMetadata(session, viewName.catalogName()) + .map(catalog -> { + CatalogHandle catalogHandle = catalog.getCatalogHandle(session, viewName, Optional.empty(), Optional.empty()); + ConnectorMetadata metadata = catalog.getMetadataFor(session, catalogHandle); + ConnectorSession connectorSession = session.toConnectorSession(catalogHandle); + return metadata.isView(connectorSession, viewName.asSchemaTableName()); + }) + .orElse(false); } @Override diff --git a/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java b/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java index 09425d19da90..fb452c26099e 100644 --- a/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java +++ b/core/trino-main/src/main/java/io/trino/tracing/TracingConnectorMetadata.java @@ -870,6 +870,15 @@ public Optional getView(ConnectorSession session, Schem } } + @Override + public boolean isView(ConnectorSession session, SchemaTableName viewName) + { + Span span = startSpan("isView", viewName); + try (var _ = scopedSpan(span)) { + return delegate.isView(session, viewName); + } + } + @Override public Map getViewProperties(ConnectorSession session, SchemaTableName viewName) { diff --git a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java index be6856872441..056df10dbf7a 100644 --- a/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java +++ b/core/trino-main/src/test/java/io/trino/execution/BaseDataDefinitionTaskTest.java @@ -541,6 +541,12 @@ public Optional getView(Session session, QualifiedObjectName vie return Optional.ofNullable(views.get(viewName.asSchemaTableName())); } + @Override + public boolean isView(Session session, QualifiedObjectName viewName) + { + return getView(session, viewName).isPresent(); + } + @Override public void createView(Session session, QualifiedObjectName viewName, ViewDefinition definition, Map viewProperties, boolean replace) { diff --git a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java index 998bf1582331..8bbf32c331cd 100644 --- a/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java +++ b/core/trino-main/src/test/java/io/trino/metadata/AbstractMockMetadata.java @@ -589,6 +589,12 @@ public Optional getView(Session session, QualifiedObjectName vie throw new UnsupportedOperationException(); } + @Override + public boolean isView(Session session, QualifiedObjectName viewName) + { + throw new UnsupportedOperationException(); + } + @Override public Map getViewProperties(Session session, QualifiedObjectName viewName) { diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java index df1587f005d8..381d46d2ce13 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java @@ -1022,6 +1022,14 @@ default Optional getView(ConnectorSession session, Sche return Optional.empty(); } + /** + * Is the specified table a view. + */ + default boolean isView(ConnectorSession session, SchemaTableName viewName) + { + return getView(session, viewName).isPresent(); + } + /** * Gets the view properties for the specified view. */ diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java index 0f9975b16953..2abb88e97b74 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java @@ -736,6 +736,14 @@ public Optional getView(ConnectorSession session, Schem } } + @Override + public boolean isView(ConnectorSession session, SchemaTableName viewName) + { + try (ThreadContextClassLoader _ = new ThreadContextClassLoader(classLoader)) { + return delegate.isView(session, viewName); + } + } + @Override public Map getViewProperties(ConnectorSession session, SchemaTableName viewName) { diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergErrorCode.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergErrorCode.java index 6822462387b3..edb866436aa5 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergErrorCode.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergErrorCode.java @@ -41,6 +41,7 @@ public enum IcebergErrorCode ICEBERG_WRITER_CLOSE_ERROR(14, EXTERNAL), ICEBERG_MISSING_METADATA(15, EXTERNAL), ICEBERG_WRITER_DATA_ERROR(16, EXTERNAL), + ICEBERG_UNSUPPORTED_VIEW_DIALECT(17, EXTERNAL) /**/; private final ErrorCode errorCode; diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java index b2bad3c97069..648a5355f404 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergMetadata.java @@ -218,6 +218,7 @@ import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_FILESYSTEM_ERROR; import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_INVALID_METADATA; import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_MISSING_METADATA; +import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_UNSUPPORTED_VIEW_DIALECT; import static io.trino.plugin.iceberg.IcebergMetadataColumn.FILE_MODIFIED_TIME; import static io.trino.plugin.iceberg.IcebergMetadataColumn.FILE_PATH; import static io.trino.plugin.iceberg.IcebergMetadataColumn.isMetadataColumnId; @@ -2504,6 +2505,20 @@ public Map getViews(ConnectorSession s return catalog.getViews(session, schemaName); } + @Override + public boolean isView(ConnectorSession session, SchemaTableName viewName) + { + try { + return catalog.getView(session, viewName).isPresent(); + } + catch (TrinoException e) { + if (e.getErrorCode() == ICEBERG_UNSUPPORTED_VIEW_DIALECT.toErrorCode()) { + return true; + } + throw e; + } + } + @Override public Optional getView(ConnectorSession session, SchemaTableName viewName) { diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergUtil.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergUtil.java index ab806b9b7b90..e3c1e391f7dd 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergUtil.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergUtil.java @@ -134,6 +134,7 @@ import static io.trino.spi.StandardErrorCode.INVALID_ARGUMENTS; import static io.trino.spi.StandardErrorCode.INVALID_TABLE_PROPERTY; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; +import static io.trino.spi.connector.ConnectorViewDefinition.ViewColumn; import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; import static io.trino.spi.function.InvocationConvention.InvocationReturnConvention.FAIL_ON_NULL; import static io.trino.spi.predicate.Utils.nativeValueToBlock; @@ -314,6 +315,18 @@ public static List getColumnMetadatas(Schema schema, TypeManager return columns.build(); } + public static Schema updateColumnComment(Schema schema, String columnName, String comment) + { + NestedField fieldToUpdate = schema.findField(columnName); + checkArgument(fieldToUpdate != null, "Field %s does not exist", columnName); + NestedField updatedField = NestedField.of(fieldToUpdate.fieldId(), fieldToUpdate.isOptional(), fieldToUpdate.name(), fieldToUpdate.type(), comment); + List newFields = schema.columns().stream() + .map(field -> (field.fieldId() == updatedField.fieldId()) ? updatedField : field) + .toList(); + + return new Schema(newFields, schema.getAliases(), schema.identifierFieldIds()); + } + public static IcebergColumnHandle getColumnHandle(NestedField column, TypeManager typeManager) { Type type = toTrinoType(column.type(), typeManager); @@ -678,6 +691,27 @@ public static Schema schemaFromMetadata(List columns) return new Schema(icebergSchema.asStructType().fields()); } + public static Schema schemaFromViewColumns(TypeManager typeManager, List columns) + { + List icebergColumns = new ArrayList<>(); + AtomicInteger nextFieldId = new AtomicInteger(1); + for (ViewColumn column : columns) { + Type trinoType = typeManager.getType(column.getType()); + org.apache.iceberg.types.Type type = toIcebergTypeForNewColumn(trinoType, nextFieldId); + NestedField field = NestedField.of(nextFieldId.getAndIncrement(), false, column.getName(), type, column.getComment().orElse(null)); + icebergColumns.add(field); + } + org.apache.iceberg.types.Type icebergSchema = StructType.of(icebergColumns); + return new Schema(icebergSchema.asStructType().fields()); + } + + public static List viewColumnsFromSchema(TypeManager typeManager, Schema schema) + { + return IcebergUtil.getColumns(schema, typeManager).stream() + .map(column -> new ViewColumn(column.getName(), column.getType().getTypeId(), column.getComment())) + .toList(); + } + public static Transaction newCreateTableTransaction(TrinoCatalog catalog, ConnectorTableMetadata tableMetadata, ConnectorSession session, boolean replace, String tableLocation) { SchemaTableName schemaTableName = tableMetadata.getTable(); diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java index 5441aa1162d0..1483ed464158 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/AbstractTrinoCatalog.java @@ -120,6 +120,8 @@ public abstract class AbstractTrinoCatalog implements TrinoCatalog { public static final String TRINO_CREATED_BY_VALUE = "Trino Iceberg connector"; + public static final String ICEBERG_VIEW_RUN_AS_OWNER = "trino.run-as-owner"; + protected static final String TRINO_CREATED_BY = HiveMetadata.TRINO_CREATED_BY; protected static final String TRINO_QUERY_ID_NAME = HiveMetadata.TRINO_QUERY_ID_NAME; diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoIcebergRestCatalogFactory.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoIcebergRestCatalogFactory.java index 43e597d26889..6deba5bde455 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoIcebergRestCatalogFactory.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoIcebergRestCatalogFactory.java @@ -25,6 +25,7 @@ import io.trino.plugin.iceberg.fileio.ForwardingFileIo; import io.trino.spi.catalog.CatalogName; import io.trino.spi.security.ConnectorIdentity; +import io.trino.spi.type.TypeManager; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.rest.HTTPClient; import org.apache.iceberg.rest.RESTSessionCatalog; @@ -46,6 +47,7 @@ public class TrinoIcebergRestCatalogFactory private final boolean vendedCredentialsEnabled; private final SecurityProperties securityProperties; private final boolean uniqueTableLocation; + private final TypeManager typeManager; @GuardedBy("this") private RESTSessionCatalog icebergCatalog; @@ -57,6 +59,7 @@ public TrinoIcebergRestCatalogFactory( IcebergRestCatalogConfig restConfig, SecurityProperties securityProperties, IcebergConfig icebergConfig, + TypeManager typeManager, NodeVersion nodeVersion) { this.fileSystemFactory = requireNonNull(fileSystemFactory, "fileSystemFactory is null"); @@ -70,6 +73,7 @@ public TrinoIcebergRestCatalogFactory( this.securityProperties = requireNonNull(securityProperties, "securityProperties is null"); requireNonNull(icebergConfig, "icebergConfig is null"); this.uniqueTableLocation = icebergConfig.isUniqueTableLocation(); + this.typeManager = requireNonNull(typeManager, "typeManager is null"); } @Override @@ -101,6 +105,6 @@ public synchronized TrinoCatalog create(ConnectorIdentity identity) icebergCatalog = icebergCatalogInstance; } - return new TrinoRestCatalog(icebergCatalog, catalogName, sessionType, trinoVersion, uniqueTableLocation); + return new TrinoRestCatalog(icebergCatalog, catalogName, sessionType, trinoVersion, typeManager, uniqueTableLocation); } } diff --git a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoRestCatalog.java b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoRestCatalog.java index 16e38b2fa9ed..929461594aab 100644 --- a/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoRestCatalog.java +++ b/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/catalog/rest/TrinoRestCatalog.java @@ -24,6 +24,7 @@ import io.trino.plugin.hive.metastore.TableInfo; import io.trino.plugin.iceberg.ColumnIdentity; import io.trino.plugin.iceberg.IcebergSchemaProperties; +import io.trino.plugin.iceberg.IcebergUtil; import io.trino.plugin.iceberg.catalog.TrinoCatalog; import io.trino.plugin.iceberg.catalog.rest.IcebergRestCatalogConfig.SessionType; import io.trino.spi.TrinoException; @@ -38,7 +39,9 @@ import io.trino.spi.connector.SchemaNotFoundException; import io.trino.spi.connector.SchemaTableName; import io.trino.spi.connector.TableNotFoundException; +import io.trino.spi.connector.ViewNotFoundException; import io.trino.spi.security.TrinoPrincipal; +import io.trino.spi.type.TypeManager; import org.apache.iceberg.BaseTable; import org.apache.iceberg.PartitionSpec; import org.apache.iceberg.Schema; @@ -52,9 +55,17 @@ import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.iceberg.exceptions.NoSuchViewException; import org.apache.iceberg.exceptions.RESTException; import org.apache.iceberg.rest.RESTSessionCatalog; import org.apache.iceberg.rest.auth.OAuth2Properties; +import org.apache.iceberg.view.ReplaceViewVersion; +import org.apache.iceberg.view.SQLViewRepresentation; +import org.apache.iceberg.view.UpdateViewProperties; +import org.apache.iceberg.view.View; +import org.apache.iceberg.view.ViewBuilder; +import org.apache.iceberg.view.ViewRepresentation; +import org.apache.iceberg.view.ViewVersion; import java.util.Date; import java.util.Iterator; @@ -71,11 +82,14 @@ import static io.trino.filesystem.Locations.appendPath; import static io.trino.plugin.hive.HiveMetadata.TABLE_COMMENT; import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_CATALOG_ERROR; +import static io.trino.plugin.iceberg.IcebergErrorCode.ICEBERG_UNSUPPORTED_VIEW_DIALECT; import static io.trino.plugin.iceberg.IcebergUtil.quotedTableName; +import static io.trino.plugin.iceberg.catalog.AbstractTrinoCatalog.ICEBERG_VIEW_RUN_AS_OWNER; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static java.util.UUID.randomUUID; +import static org.apache.iceberg.view.ViewProperties.COMMENT; public class TrinoRestCatalog implements TrinoCatalog @@ -84,6 +98,7 @@ public class TrinoRestCatalog private final RESTSessionCatalog restSessionCatalog; private final CatalogName catalogName; + private final TypeManager typeManager; private final SessionType sessionType; private final String trinoVersion; private final boolean useUniqueTableLocation; @@ -97,12 +112,14 @@ public TrinoRestCatalog( CatalogName catalogName, SessionType sessionType, String trinoVersion, + TypeManager typeManager, boolean useUniqueTableLocation) { this.restSessionCatalog = requireNonNull(restSessionCatalog, "restSessionCatalog is null"); this.catalogName = requireNonNull(catalogName, "catalogName is null"); this.sessionType = requireNonNull(sessionType, "sessionType is null"); this.trinoVersion = requireNonNull(trinoVersion, "trinoVersion is null"); + this.typeManager = requireNonNull(typeManager, "typeManager is null"); this.useUniqueTableLocation = useUniqueTableLocation; } @@ -188,10 +205,12 @@ public List listTables(ConnectorSession session, Optional nam ImmutableList.Builder tables = ImmutableList.builder(); for (Namespace restNamespace : namespaces) { try { - // views and materialized views are currently not supported, so everything is a table restSessionCatalog.listTables(sessionContext, restNamespace).stream() .map(id -> new TableInfo(SchemaTableName.schemaTableName(id.namespace().toString(), id.name()), TableInfo.ExtendedRelationType.TABLE)) .forEach(tables::add); + restSessionCatalog.listViews(sessionContext, restNamespace).stream() + .map(id -> new TableInfo(SchemaTableName.schemaTableName(id.namespace().toString(), id.name()), TableInfo.ExtendedRelationType.OTHER_VIEW)) + .forEach(tables::add); } catch (NoSuchNamespaceException e) { // Namespace may have been deleted during listing @@ -373,13 +392,30 @@ public void setTablePrincipal(ConnectorSession session, SchemaTableName schemaTa @Override public void createView(ConnectorSession session, SchemaTableName schemaViewName, ConnectorViewDefinition definition, boolean replace) { - throw new TrinoException(NOT_SUPPORTED, "createView is not supported for Iceberg REST catalog"); + ImmutableMap.Builder properties = ImmutableMap.builder(); + definition.getOwner().ifPresent(owner -> properties.put(ICEBERG_VIEW_RUN_AS_OWNER, owner)); + definition.getComment().ifPresent(comment -> properties.put(COMMENT, comment)); + Schema schema = IcebergUtil.schemaFromViewColumns(typeManager, definition.getColumns()); + ViewBuilder viewBuilder = restSessionCatalog.buildView(convert(session), toIdentifier(schemaViewName)); + viewBuilder = viewBuilder.withSchema(schema) + .withQuery("trino", definition.getOriginalSql()) + .withDefaultNamespace(Namespace.of(schemaViewName.getSchemaName())) + .withDefaultCatalog(definition.getCatalog().orElse(null)) + .withProperties(properties.buildOrThrow()) + .withLocation(defaultTableLocation(session, schemaViewName)); + + if (replace) { + viewBuilder.createOrReplace(); + } + else { + viewBuilder.create(); + } } @Override public void renameView(ConnectorSession session, SchemaTableName source, SchemaTableName target) { - throw new TrinoException(NOT_SUPPORTED, "renameView is not supported for Iceberg REST catalog"); + restSessionCatalog.renameView(convert(session), toIdentifier(source), toIdentifier(target)); } @Override @@ -391,19 +427,55 @@ public void setViewPrincipal(ConnectorSession session, SchemaTableName schemaVie @Override public void dropView(ConnectorSession session, SchemaTableName schemaViewName) { - throw new TrinoException(NOT_SUPPORTED, "dropView is not supported for Iceberg REST catalog"); + restSessionCatalog.dropView(convert(session), toIdentifier(schemaViewName)); } @Override public Map getViews(ConnectorSession session, Optional namespace) { - return ImmutableMap.of(); + SessionContext sessionContext = convert(session); + ImmutableMap.Builder views = ImmutableMap.builder(); + for (Namespace restNamespace : listNamespaces(session, namespace)) { + for (TableIdentifier restView : restSessionCatalog.listViews(sessionContext, restNamespace)) { + SchemaTableName schemaTableName = SchemaTableName.schemaTableName(restView.namespace().toString(), restView.name()); + getView(session, schemaTableName).ifPresent(view -> views.put(schemaTableName, view)); + } + } + + return views.buildOrThrow(); } @Override public Optional getView(ConnectorSession session, SchemaTableName viewName) { - return Optional.empty(); + return getIcebergView(session, viewName).flatMap(view -> { + SQLViewRepresentation sqlView = view.sqlFor("trino"); + if (!sqlView.dialect().equalsIgnoreCase("trino")) { + throw new TrinoException(ICEBERG_UNSUPPORTED_VIEW_DIALECT, "Cannot read unsupported dialect '%s' for view '%s'".formatted(sqlView.dialect(), viewName)); + } + + Optional comment = Optional.ofNullable(view.properties().get(COMMENT)); + List viewColumns = IcebergUtil.viewColumnsFromSchema(typeManager, view.schema()); + ViewVersion currentVersion = view.currentVersion(); + Optional catalog = Optional.ofNullable(currentVersion.defaultCatalog()); + Optional schema = Optional.empty(); + if (catalog.isPresent() && !currentVersion.defaultNamespace().isEmpty()) { + schema = Optional.of(currentVersion.defaultNamespace().toString()); + } + + Optional owner = Optional.ofNullable(view.properties().get(ICEBERG_VIEW_RUN_AS_OWNER)); + return Optional.of(new ConnectorViewDefinition(sqlView.sql(), catalog, schema, viewColumns, comment, owner, owner.isEmpty(), null)); + }); + } + + private Optional getIcebergView(ConnectorSession session, SchemaTableName viewName) + { + try { + return Optional.of(restSessionCatalog.loadView(convert(session), toIdentifier(viewName))); + } + catch (NoSuchViewException e) { + return Optional.empty(); + } } @Override @@ -471,13 +543,33 @@ public Optional redirectTable(ConnectorSession session, @Override public void updateViewComment(ConnectorSession session, SchemaTableName schemaViewName, Optional comment) { - throw new TrinoException(NOT_SUPPORTED, "updateViewComment is not supported for Iceberg REST catalog"); + View view = getIcebergView(session, schemaViewName).orElseThrow(() -> new ViewNotFoundException(schemaViewName)); + UpdateViewProperties updateViewProperties = view.updateProperties(); + comment.ifPresentOrElse( + value -> updateViewProperties.set(COMMENT, value), + () -> updateViewProperties.remove(COMMENT)); + updateViewProperties.commit(); } @Override public void updateViewColumnComment(ConnectorSession session, SchemaTableName schemaViewName, String columnName, Optional comment) { - throw new TrinoException(NOT_SUPPORTED, "updateViewColumnComment is not supported for Iceberg REST catalog"); + View view = getIcebergView(session, schemaViewName) + .orElseThrow(() -> new ViewNotFoundException(schemaViewName)); + + ViewVersion current = view.currentVersion(); + Schema updatedSchema = IcebergUtil.updateColumnComment(view.schema(), columnName, comment.orElse(null)); + ReplaceViewVersion replaceViewVersion = view.replaceVersion() + .withSchema(updatedSchema) + .withDefaultCatalog(current.defaultCatalog()) + .withDefaultNamespace(current.defaultNamespace()); + for (ViewRepresentation representation : view.currentVersion().representations()) { + if (representation instanceof SQLViewRepresentation sqlViewRepresentation) { + replaceViewVersion.withQuery(sqlViewRepresentation.dialect(), sqlViewRepresentation.sql()); + } + } + + replaceViewVersion.commit(); } private SessionCatalog.SessionContext convert(ConnectorSession session) diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java index 3e6e873f353a..b32afe4975cb 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/BaseTrinoCatalogTest.java @@ -347,7 +347,7 @@ public void testView() Optional.empty(), Optional.empty(), ImmutableList.of( - new ConnectorViewDefinition.ViewColumn("name", VarcharType.createVarcharType(25).getTypeId(), Optional.empty())), + new ConnectorViewDefinition.ViewColumn("name", VarcharType.createUnboundedVarcharType().getTypeId(), Optional.empty())), Optional.empty(), Optional.of(SESSION.getUser()), false, @@ -434,6 +434,19 @@ public void testListTables() } } + protected void assertViewDefinition(ConnectorViewDefinition actualView, ConnectorViewDefinition expectedView) + { + assertThat(actualView.getOriginalSql()).isEqualTo(expectedView.getOriginalSql()); + assertThat(actualView.getCatalog()).isEqualTo(expectedView.getCatalog()); + assertThat(actualView.getSchema()).isEqualTo(expectedView.getSchema()); + assertThat(actualView.getColumns().size()).isEqualTo(expectedView.getColumns().size()); + for (int i = 0; i < actualView.getColumns().size(); i++) { + assertViewColumnDefinition(actualView.getColumns().get(i), expectedView.getColumns().get(i)); + } + assertThat(actualView.getOwner()).isEqualTo(expectedView.getOwner()); + assertThat(actualView.isRunAsInvoker()).isEqualTo(expectedView.isRunAsInvoker()); + } + private String arbitraryTableLocation(TrinoCatalog catalog, ConnectorSession session, SchemaTableName schemaTableName) throws Exception { @@ -450,19 +463,6 @@ private String arbitraryTableLocation(TrinoCatalog catalog, ConnectorSession ses return tmpDirectory.toString(); } - private void assertViewDefinition(ConnectorViewDefinition actualView, ConnectorViewDefinition expectedView) - { - assertThat(actualView.getOriginalSql()).isEqualTo(expectedView.getOriginalSql()); - assertThat(actualView.getCatalog()).isEqualTo(expectedView.getCatalog()); - assertThat(actualView.getSchema()).isEqualTo(expectedView.getSchema()); - assertThat(actualView.getColumns().size()).isEqualTo(expectedView.getColumns().size()); - for (int i = 0; i < actualView.getColumns().size(); i++) { - assertViewColumnDefinition(actualView.getColumns().get(i), expectedView.getColumns().get(i)); - } - assertThat(actualView.getOwner()).isEqualTo(expectedView.getOwner()); - assertThat(actualView.isRunAsInvoker()).isEqualTo(expectedView.isRunAsInvoker()); - } - private void assertViewColumnDefinition(ConnectorViewDefinition.ViewColumn actualViewColumn, ConnectorViewDefinition.ViewColumn expectedViewColumn) { assertThat(actualViewColumn.getName()).isEqualTo(expectedViewColumn.getName()); diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/RestCatalogTestUtils.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/RestCatalogTestUtils.java index cc0961b86804..0db090caa647 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/RestCatalogTestUtils.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/RestCatalogTestUtils.java @@ -42,6 +42,7 @@ public static Catalog backendCatalog(File warehouseLocation) properties.put(CatalogProperties.URI, "jdbc:h2:file:" + Files.newTemporaryFile().getAbsolutePath()); properties.put(JdbcCatalog.PROPERTY_PREFIX + "username", "user"); properties.put(JdbcCatalog.PROPERTY_PREFIX + "password", "password"); + properties.put(JdbcCatalog.PROPERTY_PREFIX + "schema-version", "V1"); properties.put(CatalogProperties.WAREHOUSE_LOCATION, warehouseLocation.toPath().resolve("iceberg_data").toFile().getAbsolutePath()); ConnectorSession connectorSession = TestingConnectorSession.builder().build(); diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergTrinoRestCatalogConnectorSmokeTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergTrinoRestCatalogConnectorSmokeTest.java index 9563ffeb391d..fbbd57ccca5c 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergTrinoRestCatalogConnectorSmokeTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergTrinoRestCatalogConnectorSmokeTest.java @@ -63,12 +63,9 @@ public TestIcebergTrinoRestCatalogConnectorSmokeTest() protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) { return switch (connectorBehavior) { - case SUPPORTS_COMMENT_ON_VIEW, - SUPPORTS_COMMENT_ON_VIEW_COLUMN, - SUPPORTS_CREATE_MATERIALIZED_VIEW, - SUPPORTS_CREATE_VIEW, - SUPPORTS_RENAME_MATERIALIZED_VIEW, - SUPPORTS_RENAME_SCHEMA -> false; + case SUPPORTS_CREATE_MATERIALIZED_VIEW, + SUPPORTS_RENAME_MATERIALIZED_VIEW, + SUPPORTS_RENAME_SCHEMA -> false; default -> super.hasBehavior(connectorBehavior); }; } @@ -110,14 +107,6 @@ public void teardown() backend = null; // closed by closeAfterClass } - @Test - @Override - public void testView() - { - assertThatThrownBy(super::testView) - .hasMessageContaining("createView is not supported for Iceberg REST catalog"); - } - @Test @Override public void testMaterializedView() diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergVendingRestCatalogConnectorSmokeTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergVendingRestCatalogConnectorSmokeTest.java index 1bee806bc9fc..615135bdf17a 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergVendingRestCatalogConnectorSmokeTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestIcebergVendingRestCatalogConnectorSmokeTest.java @@ -77,12 +77,9 @@ public TestIcebergVendingRestCatalogConnectorSmokeTest() protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) { return switch (connectorBehavior) { - case SUPPORTS_COMMENT_ON_VIEW, - SUPPORTS_COMMENT_ON_VIEW_COLUMN, - SUPPORTS_CREATE_MATERIALIZED_VIEW, - SUPPORTS_CREATE_VIEW, - SUPPORTS_RENAME_MATERIALIZED_VIEW, - SUPPORTS_RENAME_SCHEMA -> false; + case SUPPORTS_CREATE_MATERIALIZED_VIEW, + SUPPORTS_RENAME_MATERIALIZED_VIEW, + SUPPORTS_RENAME_SCHEMA -> false; default -> super.hasBehavior(connectorBehavior); }; } @@ -145,14 +142,6 @@ public void initFileSystem() ).create(SESSION); } - @Test - @Override - public void testView() - { - assertThatThrownBy(super::testView) - .hasMessageContaining("createView is not supported for Iceberg REST catalog"); - } - @Test @Override public void testMaterializedView() @@ -309,14 +298,6 @@ public void testDropTableWithMissingManifestListFile() .hasMessageContaining("Table location should not exist"); } - @Test - @Override - public void testDropTableWithMissingDataFile() - { - assertThatThrownBy(super::testDropTableWithMissingDataFile) - .hasMessageContaining("Table location should not exist"); - } - @Test @Override public void testDropTableWithNonExistentTableLocation() diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestTrinoRestCatalog.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestTrinoRestCatalog.java index a26fffb2c244..6c69637ddc66 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestTrinoRestCatalog.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/catalog/rest/TestTrinoRestCatalog.java @@ -13,8 +13,11 @@ */ package io.trino.plugin.iceberg.catalog.rest; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.airlift.log.Logger; import io.trino.plugin.hive.NodeVersion; +import io.trino.plugin.hive.metastore.TableInfo; import io.trino.plugin.iceberg.CommitTaskData; import io.trino.plugin.iceberg.IcebergMetadata; import io.trino.plugin.iceberg.TableStatisticsWriter; @@ -23,16 +26,25 @@ import io.trino.spi.catalog.CatalogName; import io.trino.spi.connector.CatalogHandle; import io.trino.spi.connector.ConnectorMetadata; +import io.trino.spi.connector.ConnectorViewDefinition; +import io.trino.spi.connector.SchemaTableName; import io.trino.spi.security.PrincipalType; import io.trino.spi.security.TrinoPrincipal; +import io.trino.spi.type.TestingTypeManager; +import io.trino.spi.type.VarcharType; import org.apache.iceberg.rest.DelegatingRestSessionCatalog; import org.apache.iceberg.rest.RESTSessionCatalog; import org.assertj.core.util.Files; import org.junit.jupiter.api.Test; import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; import static io.airlift.json.JsonCodec.jsonCodec; +import static io.trino.plugin.hive.metastore.TableInfo.ExtendedRelationType.OTHER_VIEW; import static io.trino.plugin.iceberg.catalog.rest.IcebergRestCatalogConfig.SessionType.NONE; import static io.trino.plugin.iceberg.catalog.rest.RestCatalogTestUtils.backendCatalog; import static io.trino.sql.planner.TestingPlannerContext.PLANNER_CONTEXT; @@ -40,11 +52,12 @@ import static io.trino.testing.TestingNames.randomNameSuffix; import static java.util.Locale.ENGLISH; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; public class TestTrinoRestCatalog extends BaseTrinoCatalogTest { + private static final Logger LOG = Logger.get(TestTrinoRestCatalog.class); + @Override protected TrinoCatalog createTrinoCatalog(boolean useUniqueTableLocations) { @@ -59,15 +72,7 @@ protected TrinoCatalog createTrinoCatalog(boolean useUniqueTableLocations) restSessionCatalog.initialize(catalogName, ImmutableMap.of()); - return new TrinoRestCatalog(restSessionCatalog, new CatalogName(catalogName), NONE, "test", useUniqueTableLocations); - } - - @Test - @Override - public void testView() - { - assertThatThrownBy(super::testView) - .hasMessageContaining("createView is not supported for Iceberg REST catalog"); + return new TrinoRestCatalog(restSessionCatalog, new CatalogName(catalogName), NONE, "test", new TestingTypeManager(), useUniqueTableLocations); } @Test @@ -112,4 +117,62 @@ public void testNonLowercaseNamespace() catalog.dropNamespace(SESSION, namespace); } } + + @Test + @Override + public void testView() + throws IOException + { + TrinoCatalog catalog = createTrinoCatalog(false); + Path tmpDirectory = java.nio.file.Files.createTempDirectory("iceberg_catalog_test_create_view_"); + tmpDirectory.toFile().deleteOnExit(); + + String namespace = "test_create_view_" + randomNameSuffix(); + String viewName = "viewName"; + String renamedViewName = "renamedViewName"; + SchemaTableName schemaTableName = new SchemaTableName(namespace, viewName); + SchemaTableName renamedSchemaTableName = new SchemaTableName(namespace, renamedViewName); + ConnectorViewDefinition viewDefinition = new ConnectorViewDefinition( + "SELECT name FROM local.tiny.nation", + Optional.empty(), + Optional.empty(), + ImmutableList.of( + new ConnectorViewDefinition.ViewColumn("name", VarcharType.createUnboundedVarcharType().getTypeId(), Optional.empty())), + Optional.empty(), + Optional.of(SESSION.getUser()), + false, + ImmutableList.of()); + + try { + catalog.createNamespace(SESSION, namespace, ImmutableMap.of(), new TrinoPrincipal(PrincipalType.USER, SESSION.getUser())); + catalog.createView(SESSION, schemaTableName, viewDefinition, false); + + assertThat(catalog.listTables(SESSION, Optional.of(namespace)).stream()).contains(new TableInfo(schemaTableName, OTHER_VIEW)); + + Map views = catalog.getViews(SESSION, Optional.of(schemaTableName.getSchemaName())); + assertThat(views.size()).isEqualTo(1); + assertViewDefinition(views.get(schemaTableName), viewDefinition); + assertViewDefinition(catalog.getView(SESSION, schemaTableName).orElseThrow(), viewDefinition); + + catalog.renameView(SESSION, schemaTableName, renamedSchemaTableName); + assertThat(catalog.listTables(SESSION, Optional.of(namespace)).stream().map(TableInfo::tableName).toList()).doesNotContain(schemaTableName); + views = catalog.getViews(SESSION, Optional.of(schemaTableName.getSchemaName())); + assertThat(views.size()).isEqualTo(1); + assertViewDefinition(views.get(renamedSchemaTableName), viewDefinition); + assertViewDefinition(catalog.getView(SESSION, renamedSchemaTableName).orElseThrow(), viewDefinition); + assertThat(catalog.getView(SESSION, schemaTableName)).isEmpty(); + + catalog.dropView(SESSION, renamedSchemaTableName); + assertThat(catalog.listTables(SESSION, Optional.empty()).stream().map(TableInfo::tableName).toList()) + .doesNotContain(renamedSchemaTableName); + } + finally { + try { + catalog.dropNamespace(SESSION, namespace); + } + catch (Exception e) { + LOG.warn("Failed to clean up namespace: %s", namespace); + } + } + } } diff --git a/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/IcebergRestCatalogBackendContainer.java b/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/IcebergRestCatalogBackendContainer.java index a66e676a33df..0516d4caffb8 100644 --- a/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/IcebergRestCatalogBackendContainer.java +++ b/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/IcebergRestCatalogBackendContainer.java @@ -32,7 +32,7 @@ public IcebergRestCatalogBackendContainer( String minioSessionToken) { super( - "tabulario/iceberg-rest:0.12.0", + "tabulario/iceberg-rest:1.5.0", "iceberg-rest", ImmutableSet.of(8181), ImmutableMap.of(), diff --git a/testing/trino-tests/src/test/java/io/trino/connector/informationschema/TestInformationSchemaConnector.java b/testing/trino-tests/src/test/java/io/trino/connector/informationschema/TestInformationSchemaConnector.java index a1636452f1ae..7e8ae06e5f33 100644 --- a/testing/trino-tests/src/test/java/io/trino/connector/informationschema/TestInformationSchemaConnector.java +++ b/testing/trino-tests/src/test/java/io/trino/connector/informationschema/TestInformationSchemaConnector.java @@ -224,10 +224,10 @@ public void testMetadataCalls() .add("ConnectorMetadata.getMaterializedView(schema=test_schema2, table=test_table1)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema3_empty, table=test_table1)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema4_empty, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema2, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema3_empty, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema4_empty, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema1, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema2, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema3_empty, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema4_empty, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema2, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema3_empty, table=test_table1)") @@ -268,14 +268,14 @@ public void testMetadataCalls() .add("ConnectorMetadata.getMaterializedView(schema=test_schema3_empty, table=test_table1)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema4_empty, table=test_table2)") .add("ConnectorMetadata.getMaterializedView(schema=test_schema4_empty, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema1, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema1, table=test_table2)") - .add("ConnectorMetadata.getView(schema=test_schema2, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema2, table=test_table2)") - .add("ConnectorMetadata.getView(schema=test_schema3_empty, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema3_empty, table=test_table2)") - .add("ConnectorMetadata.getView(schema=test_schema4_empty, table=test_table1)") - .add("ConnectorMetadata.getView(schema=test_schema4_empty, table=test_table2)") + .add("ConnectorMetadata.isView(schema=test_schema1, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema1, table=test_table2)") + .add("ConnectorMetadata.isView(schema=test_schema2, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema2, table=test_table2)") + .add("ConnectorMetadata.isView(schema=test_schema3_empty, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema3_empty, table=test_table2)") + .add("ConnectorMetadata.isView(schema=test_schema4_empty, table=test_table1)") + .add("ConnectorMetadata.isView(schema=test_schema4_empty, table=test_table2)") .add("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table1)") .add("ConnectorMetadata.redirectTable(schema=test_schema1, table=test_table2)") .add("ConnectorMetadata.redirectTable(schema=test_schema2, table=test_table1)")