diff --git a/core/trino-main/src/main/java/io/trino/execution/AddColumnTask.java b/core/trino-main/src/main/java/io/trino/execution/AddColumnTask.java index 548d4bde61fa..433184a1ed4d 100644 --- a/core/trino-main/src/main/java/io/trino/execution/AddColumnTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/AddColumnTask.java @@ -72,7 +72,7 @@ public ListenableFuture execute( { Session session = stateMachine.getSession(); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { if (!statement.isTableExists()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); diff --git a/core/trino-main/src/main/java/io/trino/execution/CommentTask.java b/core/trino-main/src/main/java/io/trino/execution/CommentTask.java index b5f91a46ea9e..36b9527c750c 100644 --- a/core/trino-main/src/main/java/io/trino/execution/CommentTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/CommentTask.java @@ -61,7 +61,7 @@ public ListenableFuture execute( if (statement.getType() == Comment.Type.TABLE) { QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table does not exist: %s", tableName); } @@ -77,7 +77,7 @@ else if (statement.getType() == Comment.Type.COLUMN) { } QualifiedObjectName tableName = createQualifiedObjectName(session, statement, prefix.get()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table does not exist: " + tableName); } diff --git a/core/trino-main/src/main/java/io/trino/execution/CreateTableTask.java b/core/trino-main/src/main/java/io/trino/execution/CreateTableTask.java index d77d1873f2b2..bce99fd45bd8 100644 --- a/core/trino-main/src/main/java/io/trino/execution/CreateTableTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/CreateTableTask.java @@ -113,7 +113,7 @@ ListenableFuture internalExecute(CreateTable statement, Metadata metadata, Map, Expression> parameterLookup = parameterExtractor(statement, parameters); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isPresent()) { if (!statement.isNotExists()) { throw semanticException(TABLE_ALREADY_EXISTS, statement, "Table '%s' already exists", tableName); diff --git a/core/trino-main/src/main/java/io/trino/execution/DropColumnTask.java b/core/trino-main/src/main/java/io/trino/execution/DropColumnTask.java index c35a77be699f..932e10e68f20 100644 --- a/core/trino-main/src/main/java/io/trino/execution/DropColumnTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/DropColumnTask.java @@ -57,7 +57,7 @@ public ListenableFuture execute( { Session session = stateMachine.getSession(); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getTable()); - Optional tableHandleOptional = metadata.getTableHandle(session, tableName); + Optional tableHandleOptional = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandleOptional.isEmpty()) { if (!statement.isTableExists()) { diff --git a/core/trino-main/src/main/java/io/trino/execution/DropTableTask.java b/core/trino-main/src/main/java/io/trino/execution/DropTableTask.java index ed355aea2a1e..d74369777904 100644 --- a/core/trino-main/src/main/java/io/trino/execution/DropTableTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/DropTableTask.java @@ -54,7 +54,7 @@ public ListenableFuture execute( Session session = stateMachine.getSession(); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getTableName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { if (!statement.isExists()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); diff --git a/core/trino-main/src/main/java/io/trino/execution/GrantTask.java b/core/trino-main/src/main/java/io/trino/execution/GrantTask.java index 352791ef3872..6698fcb4c3f6 100644 --- a/core/trino-main/src/main/java/io/trino/execution/GrantTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/GrantTask.java @@ -89,7 +89,7 @@ private void executeGrantOnSchema(Session session, Grant statement, Metadata met private void executeGrantOnTable(Session session, Grant statement, Metadata metadata, AccessControl accessControl) { QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); } diff --git a/core/trino-main/src/main/java/io/trino/execution/RenameColumnTask.java b/core/trino-main/src/main/java/io/trino/execution/RenameColumnTask.java index a71d762df830..3e9b1ac5eb23 100644 --- a/core/trino-main/src/main/java/io/trino/execution/RenameColumnTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/RenameColumnTask.java @@ -59,7 +59,7 @@ public ListenableFuture execute( { Session session = stateMachine.getSession(); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getTable()); - Optional tableHandleOptional = metadata.getTableHandle(session, tableName); + Optional tableHandleOptional = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandleOptional.isEmpty()) { if (!statement.isTableExists()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); diff --git a/core/trino-main/src/main/java/io/trino/execution/RenameTableTask.java b/core/trino-main/src/main/java/io/trino/execution/RenameTableTask.java index efff0dd56506..dbf1b1bb0026 100644 --- a/core/trino-main/src/main/java/io/trino/execution/RenameTableTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/RenameTableTask.java @@ -56,7 +56,7 @@ public ListenableFuture execute( { Session session = stateMachine.getSession(); QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getSource()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { if (!statement.isExists()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); @@ -68,7 +68,7 @@ public ListenableFuture execute( if (metadata.getCatalogHandle(session, target.getCatalogName()).isEmpty()) { throw semanticException(CATALOG_NOT_FOUND, statement, "Target catalog '%s' does not exist", target.getCatalogName()); } - if (metadata.getTableHandle(session, target).isPresent()) { + if (metadata.getOriginalTableHandle(session, target, Optional.of(getName())).isPresent()) { throw semanticException(TABLE_ALREADY_EXISTS, statement, "Target table '%s' already exists", target); } if (!tableName.getCatalogName().equals(target.getCatalogName())) { diff --git a/core/trino-main/src/main/java/io/trino/execution/RevokeTask.java b/core/trino-main/src/main/java/io/trino/execution/RevokeTask.java index 196e69407bce..1a8d27a2fbf7 100644 --- a/core/trino-main/src/main/java/io/trino/execution/RevokeTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/RevokeTask.java @@ -89,7 +89,7 @@ private void executeRevokeOnSchema(Session session, Revoke statement, Metadata m private void executeRevokeOnTable(Session session, Revoke statement, Metadata metadata, AccessControl accessControl) { QualifiedObjectName tableName = createQualifiedObjectName(session, statement, statement.getName()); - Optional tableHandle = metadata.getTableHandle(session, tableName); + Optional tableHandle = metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())); if (tableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); } diff --git a/core/trino-main/src/main/java/io/trino/execution/SetTableAuthorizationTask.java b/core/trino-main/src/main/java/io/trino/execution/SetTableAuthorizationTask.java index 1d5fb5ba56d5..cadc3a665301 100644 --- a/core/trino-main/src/main/java/io/trino/execution/SetTableAuthorizationTask.java +++ b/core/trino-main/src/main/java/io/trino/execution/SetTableAuthorizationTask.java @@ -28,6 +28,7 @@ import io.trino.transaction.TransactionManager; import java.util.List; +import java.util.Optional; import static com.google.common.util.concurrent.Futures.immediateVoidFuture; import static io.trino.metadata.MetadataUtil.createPrincipal; @@ -61,7 +62,7 @@ public ListenableFuture execute( CatalogName catalogName = metadata.getCatalogHandle(session, tableName.getCatalogName()) .orElseThrow(() -> new TrinoException(NOT_FOUND, "Catalog does not exist: " + tableName.getCatalogName())); - if (metadata.getTableHandle(session, tableName).isEmpty()) { + if (metadata.getOriginalTableHandle(session, tableName, Optional.of(getName())).isEmpty()) { throw semanticException(TABLE_NOT_FOUND, statement, "Table '%s' does not exist", tableName); } 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 7232e9c6f7dc..2763d69ae0d2 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 @@ -87,11 +87,6 @@ public interface Metadata List listSchemaNames(Session session, String catalogName); - /** - * Returns a table handle for the specified table name. - */ - Optional getTableHandle(Session session, QualifiedObjectName tableName); - Optional getSystemTable(Session session, QualifiedObjectName tableName); Optional getTableHandleForStatisticsCollection(Session session, QualifiedObjectName tableName, Map analyzeProperties); @@ -669,4 +664,9 @@ default ResolvedFunction getCoercion(Type fromType, Type toType) * Get the target table handle after performing redirection. */ RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName); + + /** + * Get the table handle for {@param tableName} if it is not redirected. Throws an exception otherwise. + */ + Optional getOriginalTableHandle(Session session, QualifiedObjectName tableName, Optional invokerDescription); } 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 f7ca7ecba6ed..02984679b124 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 @@ -374,8 +374,7 @@ public List listSchemaNames(Session session, String catalogName) return ImmutableList.copyOf(schemaNames.build()); } - @Override - public Optional getTableHandle(Session session, QualifiedObjectName table) + private Optional getTableHandle(Session session, QualifiedObjectName table) { requireNonNull(table, "table is null"); @@ -410,6 +409,10 @@ public Optional getTableHandleForStatisticsCollection(Session sessi CatalogName catalogName = catalogMetadata.getConnectorId(session, table); ConnectorMetadata metadata = catalogMetadata.getMetadataFor(catalogName); + QualifiedObjectName targetTableName = getRedirectedTableName(session, table); + if (!targetTableName.equals(table)) { + throw new TrinoException(NOT_SUPPORTED, format("Cannot collect statistics for table '%s', because it is redirected to '%s'", table, targetTableName)); + } ConnectorTableHandle tableHandle = metadata.getTableHandleForStatisticsCollection(session.toConnectorSession(catalogName), table.asSchemaTableName(), analyzeProperties); if (tableHandle != null) { return Optional.of(new TableHandle( @@ -646,7 +649,12 @@ private boolean isExistingRelation(Session session, QualifiedObjectName name) return true; } - return getTableHandle(session, name).isPresent(); + if (!getRedirectedTableName(session, name).equals(name)) { + // If the table is redirected, we do not check for existence of the target table + return true; + } + + return getOriginalTableHandle(session, name, Optional.empty()).isPresent(); } @Override @@ -1412,6 +1420,11 @@ private QualifiedObjectName getRedirectedTableName(Session session, QualifiedObj requireNonNull(session, "session is null"); requireNonNull(originalTableName, "originalTableName is null"); + if (originalTableName.getCatalogName().isEmpty() || originalTableName.getSchemaName().isEmpty() || originalTableName.getObjectName().isEmpty()) { + // table cannot exist + return originalTableName; + } + QualifiedObjectName tableName = originalTableName; Set visitedTableNames = new LinkedHashSet<>(); visitedTableNames.add(tableName); @@ -1472,6 +1485,22 @@ public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session sessio throw new TrinoException(TABLE_REDIRECTION_ERROR, format("Table '%s' redirected to '%s', but the target table '%s' does not exist", tableName, targetTableName, targetTableName)); } + @Override + public Optional getOriginalTableHandle(Session session, QualifiedObjectName tableName, Optional invokerDescription) + { + RedirectionAwareTableHandle redirectionAwareTableHandle = getRedirectionAwareTableHandle(session, tableName); + Optional redirected = redirectionAwareTableHandle.getRedirectedTableName(); + if (redirected.isPresent()) { + throw new TrinoException(NOT_SUPPORTED, + format("Failed to %s. It is not supported on table '%s' because the table is redirected to '%s'", + invokerDescription.map(value -> format("perform operation '%s'", value)).orElse("get the table handle"), + tableName, + redirected.get())); + } + + return redirectionAwareTableHandle.getTableHandle(); + } + @Override public Optional resolveIndex(Session session, TableHandle tableHandle, Set indexableColumns, Set outputColumns, TupleDomain tupleDomain) { diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataUtil.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataUtil.java index 28e5c2d4c0b0..ae3c28461c3a 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataUtil.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataUtil.java @@ -196,13 +196,14 @@ public static PrincipalSpecification createPrincipal(TrinoPrincipal principal) throw new IllegalArgumentException("Unsupported type: " + type); } + // TODO: move this method to test public static boolean tableExists(Metadata metadata, Session session, String table) { if (session.getCatalog().isEmpty() || session.getSchema().isEmpty()) { return false; } QualifiedObjectName name = new QualifiedObjectName(session.getCatalog().get(), session.getSchema().get(), table); - return metadata.getTableHandle(session, name).isPresent(); + return metadata.getOriginalTableHandle(session, name, Optional.empty()).isPresent(); } public static class TableMetadataBuilder diff --git a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java index 7752fc3fa05c..844d65246132 100644 --- a/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java +++ b/core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java @@ -418,7 +418,7 @@ protected Scope visitInsert(Insert insert, Optional scope) Scope queryScope = analyze(insert.getQuery(), createScope(scope)); // verify the insert destination columns match the query - Optional targetTableHandle = metadata.getTableHandle(session, targetTable); + Optional targetTableHandle = metadata.getOriginalTableHandle(session, targetTable, Optional.of("INSERT")); if (targetTableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, insert, "Table '%s' does not exist", targetTable); } @@ -543,14 +543,13 @@ protected Scope visitRefreshMaterializedView(RefreshMaterializedView refreshMate } QualifiedObjectName targetTable = createQualifiedObjectName(session, refreshMaterializedView, storageName.get()); - checkStorageTableNotRedirected(targetTable); // analyze the query that creates the data Query query = parseView(optionalView.get().getOriginalSql(), name, refreshMaterializedView); Scope queryScope = process(query, scope); // verify the insert destination columns match the query - Optional targetTableHandle = metadata.getTableHandle(session, targetTable); + Optional targetTableHandle = getStorageTableHandle(targetTable, name); if (targetTableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, refreshMaterializedView, "Table '%s' does not exist", targetTable); } @@ -665,7 +664,7 @@ protected Scope visitDelete(Delete node, Optional scope) throw semanticException(NOT_SUPPORTED, node, "Deleting from views is not supported"); } - TableHandle handle = metadata.getTableHandle(session, tableName) + TableHandle handle = metadata.getOriginalTableHandle(session, tableName, Optional.of("DELETE")) .orElseThrow(() -> semanticException(TABLE_NOT_FOUND, table, "Table '%s' does not exist", tableName)); accessControl.checkCanDeleteFromTable(session.toSecurityContext(), tableName); @@ -751,7 +750,7 @@ protected Scope visitCreateTableAsSelect(CreateTableAsSelect node, Optional targetTableHandle = metadata.getTableHandle(session, targetTable); + Optional targetTableHandle = metadata.getOriginalTableHandle(session, targetTable, Optional.of("CREATE TABLE AS SELECT")); if (targetTableHandle.isPresent()) { if (node.isNotExists()) { analysis.setCreate(new Analysis.Create( @@ -1303,8 +1302,7 @@ protected Scope visitTable(Table table, Optional scope) throw semanticException(INVALID_VIEW, table, "Materialized view '%s' is fresh but does not have storage table name", name); } QualifiedObjectName storageTableName = createQualifiedObjectName(session, table, storageName.get()); - checkStorageTableNotRedirected(storageTableName); - Optional tableHandle = metadata.getTableHandle(session, storageTableName); + Optional tableHandle = getStorageTableHandle(storageTableName, name); if (tableHandle.isEmpty()) { throw semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName); } @@ -1387,11 +1385,9 @@ protected Scope visitTable(Table table, Optional scope) return tableScope; } - private void checkStorageTableNotRedirected(QualifiedObjectName source) + private Optional getStorageTableHandle(QualifiedObjectName storageTableName, QualifiedObjectName viewName) { - metadata.getRedirectionAwareTableHandle(session, source).getRedirectedTableName().ifPresent(name -> { - throw new TrinoException(NOT_SUPPORTED, format("Redirection of materialized view storage table '%s' to '%s' is not supported", source, name)); - }); + return metadata.getOriginalTableHandle(session, storageTableName, Optional.of(format("Scan of storage table '%s' for materialized view '%s'", storageTableName, viewName))); } private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Optional tableHandle, List fields, String authorization) @@ -1580,7 +1576,6 @@ private List analyzeStorageTable(Table table, List viewFields, Tab TableSchema tableSchema = metadata.getTableSchema(session, storageTable); Map columnHandles = metadata.getColumnHandles(session, storageTable); QualifiedObjectName tableName = createQualifiedObjectName(session, table, table.getName()); - checkStorageTableNotRedirected(tableName); List tableFields = analyzeTableOutputFields(table, tableName, tableSchema, columnHandles) .stream() .filter(field -> !field.isHidden()) @@ -2211,7 +2206,7 @@ protected Scope visitUpdate(Update update, Optional scope) throw semanticException(NOT_SUPPORTED, update, "Updating through views is not supported"); } - TableHandle handle = metadata.getTableHandle(session, tableName) + TableHandle handle = metadata.getOriginalTableHandle(session, tableName, Optional.of("UPDATE")) .orElseThrow(() -> semanticException(TABLE_NOT_FOUND, table, "Table '%s' does not exist", tableName)); TableMetadata tableMetadata = metadata.getTableMetadata(session, handle); diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ApplyTableScanRedirection.java b/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ApplyTableScanRedirection.java index 519a18acffcf..cfe5857b5e97 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ApplyTableScanRedirection.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ApplyTableScanRedirection.java @@ -102,9 +102,10 @@ public Result apply(TableScanNode scanNode, Captures captures, Context context) return Result.empty(); } - Optional destinationTableHandle = metadata.getTableHandle( + Optional destinationTableHandle = metadata.getOriginalTableHandle( context.getSession(), - convertFromSchemaTableName(destinationTable.getCatalogName()).apply(destinationTable.getSchemaTableName())); + convertFromSchemaTableName(destinationTable.getCatalogName()).apply(destinationTable.getSchemaTableName()), + Optional.empty()); if (destinationTableHandle.isEmpty()) { throw new TrinoException(TABLE_NOT_FOUND, format("Destination table %s from table scan redirection not found", destinationTable)); } diff --git a/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ExtractSpatialJoins.java b/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ExtractSpatialJoins.java index 1c16571d482b..5554de41949e 100644 --- a/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ExtractSpatialJoins.java +++ b/core/trino-main/src/main/java/io/trino/sql/planner/iterative/rule/ExtractSpatialJoins.java @@ -456,7 +456,7 @@ else if (alignment < 0) { private static KdbTree loadKdbTree(String tableName, Session session, Metadata metadata, SplitManager splitManager, PageSourceManager pageSourceManager) { QualifiedObjectName name = toQualifiedObjectName(tableName, session.getCatalog().get(), session.getSchema().get()); - TableHandle tableHandle = metadata.getTableHandle(session, name) + TableHandle tableHandle = metadata.getOriginalTableHandle(session, name, Optional.of("Scan KdbTree table")) .orElseThrow(() -> new TrinoException(INVALID_SPATIAL_PARTITIONING, format("Table not found: %s", name))); Map columnHandles = metadata.getColumnHandles(session, tableHandle); List visibleColumnHandles = columnHandles.values().stream() diff --git a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java index c750980a7c4d..619289642a51 100644 --- a/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java +++ b/core/trino-main/src/main/java/io/trino/sql/rewrite/ShowQueriesRewrite.java @@ -241,7 +241,7 @@ protected Node visitShowGrants(ShowGrants showGrants, Void context) QualifiedObjectName qualifiedTableName = createQualifiedObjectName(session, showGrants, tableName.get()); if (metadata.getView(session, qualifiedTableName).isEmpty() && - metadata.getTableHandle(session, qualifiedTableName).isEmpty()) { + metadata.getOriginalTableHandle(session, qualifiedTableName, Optional.of("SHOW GRANTS")).isEmpty()) { throw semanticException(TABLE_NOT_FOUND, showGrants, "Table '%s' does not exist", tableName); } @@ -499,7 +499,7 @@ protected Node visitShowCreate(ShowCreate node, Void context) throw semanticException(NOT_SUPPORTED, node, "Relation '%s' is a view, not a materialized view", objectName); } - if (metadata.getTableHandle(session, objectName).isPresent()) { + if (metadata.getOriginalTableHandle(session, objectName, Optional.ofNullable("SHOW CREATE MATERIALIZED VIEW")).isPresent()) { throw semanticException(NOT_SUPPORTED, node, "Relation '%s' is a table, not a materialized view", objectName); } @@ -533,7 +533,7 @@ protected Node visitShowCreate(ShowCreate node, Void context) Optional viewDefinition = metadata.getView(session, objectName); if (viewDefinition.isEmpty()) { - if (metadata.getTableHandle(session, objectName).isPresent()) { + if (metadata.getOriginalTableHandle(session, objectName, Optional.of("SHOW CREATE VIEW")).isPresent()) { throw semanticException(NOT_SUPPORTED, node, "Relation '%s' is a table, not a view", objectName); } throw semanticException(TABLE_NOT_FOUND, node, "View '%s' does not exist", objectName); diff --git a/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java b/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java index c39f46700492..4a2ab617a9e8 100644 --- a/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java +++ b/core/trino-main/src/test/java/io/trino/execution/TestCreateMaterializedViewTask.java @@ -26,6 +26,7 @@ import io.trino.metadata.MaterializedViewPropertyManager; import io.trino.metadata.MetadataManager; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.TableHandle; import io.trino.metadata.TableMetadata; import io.trino.metadata.TablePropertyManager; @@ -69,6 +70,7 @@ import static io.airlift.concurrent.MoreFutures.getFutureValue; import static io.trino.cost.StatsCalculator.noopStatsCalculator; import static io.trino.metadata.MetadataManager.createTestMetadataManager; +import static io.trino.metadata.RedirectionAwareTableHandle.noRedirection; import static io.trino.spi.StandardErrorCode.ALREADY_EXISTS; import static io.trino.spi.StandardErrorCode.INVALID_MATERIALIZED_VIEW_PROPERTY; import static io.trino.spi.session.PropertyMetadata.stringProperty; @@ -281,7 +283,7 @@ public TableSchema getTableSchema(Session session, TableHandle tableHandle) } @Override - public Optional getTableHandle(Session session, QualifiedObjectName tableName) + public Optional getOriginalTableHandle(Session session, QualifiedObjectName tableName, Optional invokerDescription) { if (tableName.asSchemaTableName().equals(MOCK_TABLE.getTable())) { return Optional.of( @@ -294,6 +296,12 @@ public Optional getTableHandle(Session session, QualifiedObjectName return Optional.empty(); } + @Override + public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName) + { + return noRedirection(getOriginalTableHandle(session, tableName, Optional.empty())); + } + @Override public Map getColumnHandles(Session session, TableHandle tableHandle) { diff --git a/core/trino-main/src/test/java/io/trino/execution/TestCreateTableTask.java b/core/trino-main/src/test/java/io/trino/execution/TestCreateTableTask.java index 68ccacb579b2..acf68c1d4788 100644 --- a/core/trino-main/src/test/java/io/trino/execution/TestCreateTableTask.java +++ b/core/trino-main/src/test/java/io/trino/execution/TestCreateTableTask.java @@ -26,6 +26,7 @@ import io.trino.metadata.MaterializedViewPropertyManager; import io.trino.metadata.MetadataManager; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.TableHandle; import io.trino.metadata.TableMetadata; import io.trino.metadata.TablePropertyManager; @@ -64,6 +65,7 @@ import static com.google.common.collect.Sets.immutableEnumSet; import static io.airlift.concurrent.MoreFutures.getFutureValue; import static io.trino.metadata.MetadataManager.createTestMetadataManager; +import static io.trino.metadata.RedirectionAwareTableHandle.noRedirection; import static io.trino.spi.StandardErrorCode.ALREADY_EXISTS; import static io.trino.spi.StandardErrorCode.INVALID_TABLE_PROPERTY; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; @@ -372,7 +374,7 @@ public Optional getCatalogHandle(Session session, String catalogNam } @Override - public Optional getTableHandle(Session session, QualifiedObjectName tableName) + public Optional getOriginalTableHandle(Session session, QualifiedObjectName tableName, Optional invokerDescription) { if (tableName.asSchemaTableName().equals(PARENT_TABLE.getTable())) { return Optional.of( @@ -385,6 +387,12 @@ public Optional getTableHandle(Session session, QualifiedObjectName return Optional.empty(); } + @Override + public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName) + { + return noRedirection(getOriginalTableHandle(session, tableName, Optional.empty())); + } + @Override public TableMetadata getTableMetadata(Session session, TableHandle tableHandle) { 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 76821612c29d..4d0310cb114a 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 @@ -81,7 +81,6 @@ import static io.trino.metadata.FunctionId.toFunctionId; import static io.trino.metadata.FunctionKind.SCALAR; -import static io.trino.metadata.RedirectionAwareTableHandle.noRedirection; import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_FOUND; import static io.trino.spi.type.DoubleType.DOUBLE; @@ -119,12 +118,6 @@ public List listSchemaNames(Session session, String catalogName) throw new UnsupportedOperationException(); } - @Override - public Optional getTableHandle(Session session, QualifiedObjectName tableName) - { - throw new UnsupportedOperationException(); - } - @Override public Optional getTableHandleForStatisticsCollection(Session session, QualifiedObjectName tableName, Map analyzeProperties) { @@ -898,6 +891,12 @@ public Optional applyTableScanRedirect(Sessi @Override public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName) { - return noRedirection(getTableHandle(session, tableName)); + throw new UnsupportedOperationException(); + } + + @Override + public Optional getOriginalTableHandle(Session session, QualifiedObjectName tableName, Optional invokerDescription) + { + throw new UnsupportedOperationException(); } } diff --git a/core/trino-main/src/test/java/io/trino/sql/planner/assertions/BasePushdownPlanTest.java b/core/trino-main/src/test/java/io/trino/sql/planner/assertions/BasePushdownPlanTest.java index 325b4a5d2631..6283e21b1007 100644 --- a/core/trino-main/src/test/java/io/trino/sql/planner/assertions/BasePushdownPlanTest.java +++ b/core/trino-main/src/test/java/io/trino/sql/planner/assertions/BasePushdownPlanTest.java @@ -29,13 +29,13 @@ public abstract class BasePushdownPlanTest { protected Optional getTableHandle(Session session, QualifiedObjectName objectName) { - return getQueryRunner().inTransaction(session, transactionSession -> { return getQueryRunner().getMetadata().getTableHandle(transactionSession, objectName); }); + return getQueryRunner().inTransaction(session, transactionSession -> { return getQueryRunner().getMetadata().getOriginalTableHandle(transactionSession, objectName, Optional.empty()); }); } protected Map getColumnHandles(Session session, QualifiedObjectName tableName) { return getQueryRunner().inTransaction(session, transactionSession -> { - Optional table = getQueryRunner().getMetadata().getTableHandle(transactionSession, tableName); + Optional table = getQueryRunner().getMetadata().getOriginalTableHandle(transactionSession, tableName, Optional.empty()); return getQueryRunner().getMetadata().getColumnHandles(transactionSession, table.get()); }); } diff --git a/plugin/trino-geospatial/src/test/java/io/trino/plugin/geospatial/BenchmarkSpatialJoin.java b/plugin/trino-geospatial/src/test/java/io/trino/plugin/geospatial/BenchmarkSpatialJoin.java index ac8411128ea3..6235c6518c8c 100644 --- a/plugin/trino-geospatial/src/test/java/io/trino/plugin/geospatial/BenchmarkSpatialJoin.java +++ b/plugin/trino-geospatial/src/test/java/io/trino/plugin/geospatial/BenchmarkSpatialJoin.java @@ -105,7 +105,7 @@ public void dropPointsTable() { queryRunner.inTransaction(queryRunner.getDefaultSession(), transactionSession -> { Metadata metadata = queryRunner.getMetadata(); - Optional tableHandle = metadata.getTableHandle(transactionSession, QualifiedObjectName.valueOf("memory.default.points")); + Optional tableHandle = metadata.getOriginalTableHandle(transactionSession, QualifiedObjectName.valueOf("memory.default.points"), Optional.empty()); assertTrue(tableHandle.isPresent(), "Table memory.default.points does not exist"); metadata.dropTable(transactionSession, tableHandle.get()); return null; diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveConfig.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveConfig.java index 61bfaea65d5e..780af1e9b245 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveConfig.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveConfig.java @@ -146,6 +146,8 @@ public class HiveConfig private boolean legacyHiveViewTranslation; + private Optional icebergCatalogName = Optional.empty(); + public int getMaxInitialSplits() { return maxInitialSplits; @@ -1039,4 +1041,17 @@ public boolean isLegacyHiveViewTranslation() { return this.legacyHiveViewTranslation; } + + public Optional getIcebergCatalogName() + { + return icebergCatalogName; + } + + @Config("hive.iceberg-catalog-name") + @ConfigDescription("The catalog to redirect iceberg tables to") + public HiveConfig setIcebergCatalogName(String icebergCatalogName) + { + this.icebergCatalogName = Optional.ofNullable(icebergCatalogName); + return this; + } } diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadata.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadata.java index f173dd2ef7e1..bfc178d0581e 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadata.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadata.java @@ -55,6 +55,7 @@ import io.trino.spi.block.Block; import io.trino.spi.connector.Assignment; import io.trino.spi.connector.CatalogSchemaName; +import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.ConnectorInsertTableHandle; @@ -80,6 +81,7 @@ import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortingProperty; import io.trino.spi.connector.SystemTable; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableNotFoundException; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.ViewNotFoundException; @@ -123,6 +125,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.OptionalLong; @@ -315,6 +318,7 @@ public class HiveMetadata private final boolean writesToNonManagedTablesEnabled; private final boolean createsOfNonManagedTablesEnabled; private final boolean translateHiveViews; + private final Optional icebergCatalogName; private final boolean hideDeltaLakeTables; private final String prestoVersion; private final HiveStatisticsProvider hiveStatisticsProvider; @@ -331,6 +335,7 @@ public HiveMetadata( boolean writesToNonManagedTablesEnabled, boolean createsOfNonManagedTablesEnabled, boolean translateHiveViews, + Optional icebergCatalogName, boolean hideDeltaLakeTables, TypeManager typeManager, LocationService locationService, @@ -352,6 +357,7 @@ public HiveMetadata( this.writesToNonManagedTablesEnabled = writesToNonManagedTablesEnabled; this.createsOfNonManagedTablesEnabled = createsOfNonManagedTablesEnabled; this.translateHiveViews = translateHiveViews; + this.icebergCatalogName = requireNonNull(icebergCatalogName, "icebergCatalogName is null"); this.hideDeltaLakeTables = hideDeltaLakeTables; this.prestoVersion = requireNonNull(trinoVersion, "trinoVersion is null"); this.hiveStatisticsProvider = requireNonNull(hiveStatisticsProvider, "hiveStatisticsProvider is null"); @@ -379,6 +385,8 @@ public List listSchemaNames(ConnectorSession session) public HiveTableHandle getTableHandle(ConnectorSession session, SchemaTableName tableName) { requireNonNull(tableName, "tableName is null"); + checkTableNotRedirected(session, tableName); + if (isHiveSystemSchema(tableName.getSchemaName())) { return null; } @@ -463,10 +471,20 @@ public Optional getSystemTable(ConnectorSession session, SchemaTabl return systemTable; } } - return Optional.empty(); } + void checkSourceTableNotRedirected(ConnectorSession session, SchemaTableName systemTableName, SchemaTableName sourceTableName) + { + Optional redirected = redirectTable(session, sourceTableName); + if (redirected.isPresent()) { + throw new TrinoException(NOT_SUPPORTED, + format("Querying system table '%s' is not supported, because the source table is redirected to '%s'", + new CatalogSchemaTableName(catalogName.toString(), systemTableName), + redirected.get())); + } + } + @Override public ConnectorTableMetadata getTableMetadata(ConnectorSession session, ConnectorTableHandle tableHandle) { @@ -664,28 +682,32 @@ public Map getColumnHandles(ConnectorSession session, Conn @SuppressWarnings("TryWithIdenticalCatches") @Override - public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + public Stream streamTableColumns(ConnectorSession session, SchemaTablePrefix prefix) { requireNonNull(prefix, "prefix is null"); - ImmutableMap.Builder> columns = ImmutableMap.builder(); - for (SchemaTableName tableName : listTables(session, prefix)) { - try { - columns.put(tableName, getTableMetadata(session, tableName).getColumns()); - } - catch (HiveViewNotSupportedException e) { - // view is not supported - } - catch (TableNotFoundException e) { - // table disappeared during listing operation - } - catch (TrinoException e) { - // Skip this table if there's a failure due to Hive, a bad Serde, or bad metadata - if (!e.getErrorCode().getType().equals(ErrorType.EXTERNAL)) { - throw e; - } - } - } - return columns.build(); + return listTables(session, prefix).stream() + .map(tableName -> { + try { + return redirectTable(session, tableName) + .map(CatalogSchemaTableName::getSchemaTableName) + .map(TableColumnsMetadata::forRedirectedTable) + .orElse(TableColumnsMetadata.forTable(tableName, getTableMetadata(session, tableName).getColumns())); + } + catch (HiveViewNotSupportedException e) { + // view is not supported + } + catch (TableNotFoundException e) { + // table disappeared during listing operation + } + catch (TrinoException e) { + // Skip this table if there's a failure due to Hive, a bad Serde, or bad metadata + if (!e.getErrorCode().getType().equals(ErrorType.EXTERNAL)) { + throw e; + } + } + return null; + }) + .filter(Objects::nonNull); } @Override @@ -1123,6 +1145,7 @@ public void dropColumn(ConnectorSession session, ConnectorTableHandle tableHandl @Override public void setTableAuthorization(ConnectorSession session, SchemaTableName table, TrinoPrincipal principal) { + checkTableNotRedirected(session, table); metastore.setTableOwner(new HiveIdentity(session), table.getSchemaName(), table.getTableName(), HivePrincipal.from(principal)); } @@ -2705,12 +2728,14 @@ public Set listEnabledRoles(ConnectorSession session) @Override public void grantTablePrivileges(ConnectorSession session, SchemaTableName schemaTableName, Set privileges, TrinoPrincipal grantee, boolean grantOption) { + checkTableNotRedirected(session, schemaTableName); accessControlMetadata.grantTablePrivileges(session, schemaTableName, privileges, HivePrincipal.from(grantee), grantOption); } @Override public void revokeTablePrivileges(ConnectorSession session, SchemaTableName schemaTableName, Set privileges, TrinoPrincipal grantee, boolean grantOption) { + checkTableNotRedirected(session, schemaTableName); accessControlMetadata.revokeTablePrivileges(session, schemaTableName, privileges, HivePrincipal.from(grantee), grantOption); } @@ -2996,6 +3021,37 @@ public CompletableFuture refreshMaterializedView(ConnectorSession session, Sc return hiveMaterializedViewMetadata.refreshMaterializedView(session, name); } + @Override + public Optional redirectTable(ConnectorSession session, SchemaTableName tableName) + { + if (icebergCatalogName.isPresent() + && !isHiveSystemSchema(tableName.getSchemaName()) + && isExistingIcebergTable(session, tableName)) { + return Optional.of(new CatalogSchemaTableName(icebergCatalogName.get(), tableName)); + } + + return Optional.empty(); + } + + private boolean isExistingIcebergTable(ConnectorSession session, SchemaTableName schemaTableName) + { + return metastore.getTable(new HiveIdentity(session), schemaTableName.getSchemaName(), schemaTableName.getTableName()) + .map(HiveUtil::isIcebergTable) + .orElse(false); + } + + private void checkTableNotRedirected(ConnectorSession session, SchemaTableName tableName) + { + redirectTable(session, tableName).ifPresent(targetTable -> { + throw new TrinoException(NOT_SUPPORTED, + format("This operation is not supported on '%s', because it is redirected to '%s'." + + " Modification operations (insert, alter, drop, delete, update, comment, grant, revoke)" + + " are not supported when redirection is enabled.", + tableName, + targetTable)); + }); + } + public static Optional getSourceTableNameFromSystemTable(SchemaTableName tableName) { return Stream.of(SystemTableHandler.values()) diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadataFactory.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadataFactory.java index 408d4c27ce54..f9680c76a57c 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadataFactory.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/HiveMetadataFactory.java @@ -47,6 +47,7 @@ public class HiveMetadataFactory private final boolean writesToNonManagedTablesEnabled; private final boolean createsOfNonManagedTablesEnabled; private final boolean translateHiveViews; + private final Optional icebergCatalogName; private final boolean hideDeltaLakeTables; private final long perTransactionCacheMaximumSize; private final HiveMetastore metastore; @@ -100,6 +101,7 @@ public HiveMetadataFactory( hiveConfig.isTranslateHiveViews(), hiveConfig.getPerTransactionMetastoreCacheMaximumSize(), hiveConfig.getHiveTransactionHeartbeatInterval(), + hiveConfig.getIcebergCatalogName(), metastoreConfig.isHideDeltaLakeTables(), typeManager, locationService, @@ -128,6 +130,7 @@ public HiveMetadataFactory( boolean translateHiveViews, long perTransactionCacheMaximumSize, Optional hiveTransactionHeartbeatInterval, + Optional icebergCatalogName, boolean hideDeltaLakeTables, TypeManager typeManager, LocationService locationService, @@ -146,6 +149,7 @@ public HiveMetadataFactory( this.writesToNonManagedTablesEnabled = writesToNonManagedTablesEnabled; this.createsOfNonManagedTablesEnabled = createsOfNonManagedTablesEnabled; this.translateHiveViews = translateHiveViews; + this.icebergCatalogName = requireNonNull(icebergCatalogName, "icebergCatalogName is null"); this.hideDeltaLakeTables = hideDeltaLakeTables; this.perTransactionCacheMaximumSize = perTransactionCacheMaximumSize; @@ -198,6 +202,7 @@ public TransactionalMetadata create() writesToNonManagedTablesEnabled, createsOfNonManagedTablesEnabled, translateHiveViews, + icebergCatalogName, hideDeltaLakeTables, typeManager, locationService, @@ -218,6 +223,7 @@ protected TransactionalMetadata create( boolean writesToNonManagedTablesEnabled, boolean createsOfNonManagedTablesEnabled, boolean translateHiveViews, + Optional icebergCatalogName, boolean hideDeltaLakeTables, TypeManager typeManager, LocationService locationService, @@ -237,6 +243,7 @@ protected TransactionalMetadata create( writesToNonManagedTablesEnabled, createsOfNonManagedTablesEnabled, translateHiveViews, + icebergCatalogName, hideDeltaLakeTables, typeManager, locationService, diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PartitionsSystemTableProvider.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PartitionsSystemTableProvider.java index 5627a4aa2a5d..09d48d8244b2 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PartitionsSystemTableProvider.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PartitionsSystemTableProvider.java @@ -63,6 +63,7 @@ public Optional getSystemTable(HiveMetadata metadata, ConnectorSess } SchemaTableName sourceTableName = PARTITIONS.getSourceTableName(tableName); + metadata.checkSourceTableNotRedirected(session, tableName, sourceTableName); HiveTableHandle sourceTableHandle = metadata.getTableHandle(session, sourceTableName); if (sourceTableHandle == null) { diff --git a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PropertiesSystemTableProvider.java b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PropertiesSystemTableProvider.java index 156a21a7d9af..86b1c819c08e 100644 --- a/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PropertiesSystemTableProvider.java +++ b/plugin/trino-hive/src/main/java/io/trino/plugin/hive/PropertiesSystemTableProvider.java @@ -47,6 +47,8 @@ public Optional getSystemTable(HiveMetadata metadata, ConnectorSess } SchemaTableName sourceTableName = PROPERTIES.getSourceTableName(tableName); + metadata.checkSourceTableNotRedirected(session, tableName, sourceTableName); + Optional table = metadata.getMetastore().getTable(new HiveIdentity(session), sourceTableName.getSchemaName(), sourceTableName.getTableName()); if (table.isEmpty() || table.get().getTableType().equals(TableType.VIRTUAL_VIEW.name())) { throw new TableNotFoundException(tableName); diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/AbstractTestHive.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/AbstractTestHive.java index 686fb93d335d..4915e8b97417 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/AbstractTestHive.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/AbstractTestHive.java @@ -98,6 +98,7 @@ import io.trino.spi.connector.SchemaTableName; import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortingProperty; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableNotFoundException; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.ViewNotFoundException; @@ -813,6 +814,7 @@ protected final void setup(String databaseName, HiveConfig hiveConfig, HiveMetas false, 1000, Optional.empty(), + Optional.empty(), true, TYPE_MANAGER, locationService, @@ -1062,7 +1064,10 @@ public void testGetAllTableColumns() { try (Transaction transaction = newTransaction()) { ConnectorMetadata metadata = transaction.getMetadata(); - Map> allColumns = metadata.listTableColumns(newSession(), new SchemaTablePrefix()); + Map> allColumns = metadata.streamTableColumns(newSession(), new SchemaTablePrefix()) + .collect(toImmutableMap( + TableColumnsMetadata::getTable, + columnsMetadata -> columnsMetadata.getColumns().orElseThrow(() -> new IllegalStateException("Unexpected table redirection")))); assertTrue(allColumns.containsKey(tablePartitionFormat)); assertTrue(allColumns.containsKey(tableUnpartitioned)); } @@ -1073,7 +1078,10 @@ public void testGetAllTableColumnsInSchema() { try (Transaction transaction = newTransaction()) { ConnectorMetadata metadata = transaction.getMetadata(); - Map> allColumns = metadata.listTableColumns(newSession(), new SchemaTablePrefix(database)); + Map> allColumns = metadata.streamTableColumns(newSession(), new SchemaTablePrefix(database)) + .collect(toImmutableMap( + TableColumnsMetadata::getTable, + columnsMetadata -> columnsMetadata.getColumns().orElseThrow(() -> new IllegalStateException("Unexpected table redirection")))); assertTrue(allColumns.containsKey(tablePartitionFormat)); assertTrue(allColumns.containsKey(tableUnpartitioned)); } @@ -1087,7 +1095,7 @@ public void testListUnknownSchema() ConnectorSession session = newSession(); assertNull(metadata.getTableHandle(session, new SchemaTableName(INVALID_DATABASE, INVALID_TABLE))); assertEquals(metadata.listTables(session, Optional.of(INVALID_DATABASE)), ImmutableList.of()); - assertEquals(metadata.listTableColumns(session, new SchemaTablePrefix(INVALID_DATABASE, INVALID_TABLE)), ImmutableMap.of()); + assertEquals(metadata.streamTableColumns(session, new SchemaTablePrefix(INVALID_DATABASE, INVALID_TABLE)).collect(toImmutableList()), ImmutableList.of()); assertEquals(metadata.listViews(session, Optional.of(INVALID_DATABASE)), ImmutableList.of()); assertEquals(metadata.getViews(session, Optional.of(INVALID_DATABASE)), ImmutableMap.of()); assertEquals(metadata.getView(session, new SchemaTableName(INVALID_DATABASE, INVALID_TABLE)), Optional.empty()); @@ -1336,9 +1344,13 @@ public void testGetTableSchemaOffline() { try (Transaction transaction = newTransaction()) { ConnectorMetadata metadata = transaction.getMetadata(); - Map> columns = metadata.listTableColumns(newSession(), tableOffline.toSchemaTablePrefix()); + List columns = metadata.streamTableColumns(newSession(), tableOffline.toSchemaTablePrefix()) + .collect(toImmutableList()); assertEquals(columns.size(), 1); - Map map = uniqueIndex(getOnlyElement(columns.values()), ColumnMetadata::getName); + Map map = uniqueIndex( + getOnlyElement(columns).getColumns() + .orElseThrow(() -> new IllegalStateException("Unexpected table redirection")), + ColumnMetadata::getName); assertPrimitiveField(map, "t_string", createUnboundedVarcharType(), false); } @@ -2366,7 +2378,9 @@ public void testHiveViewsHaveNoColumns() { try (Transaction transaction = newTransaction()) { ConnectorMetadata metadata = transaction.getMetadata(); - assertEquals(metadata.listTableColumns(newSession(), new SchemaTablePrefix(view.getSchemaName(), view.getTableName())), ImmutableMap.of()); + assertEquals( + metadata.streamTableColumns(newSession(), new SchemaTablePrefix(view.getSchemaName(), view.getTableName())).collect(toImmutableList()), + ImmutableList.of()); } } @@ -2905,15 +2919,18 @@ public void testHideDeltaLakeTables() .doesNotContain(tableName); // list all columns - assertThat(metadata.listTableColumns(session, new SchemaTablePrefix()).keySet()) + assertThat(metadata.streamTableColumns(session, new SchemaTablePrefix())) + .extracting(TableColumnsMetadata::getTable) .doesNotContain(tableName); // list all columns in a schema - assertThat(metadata.listTableColumns(session, new SchemaTablePrefix(tableName.getSchemaName())).keySet()) + assertThat(metadata.streamTableColumns(session, new SchemaTablePrefix(tableName.getSchemaName()))) + .extracting(TableColumnsMetadata::getTable) .doesNotContain(tableName); // list all columns in a table - assertThat(metadata.listTableColumns(session, new SchemaTablePrefix(tableName.getSchemaName(), tableName.getTableName())).keySet()) + assertThat(metadata.streamTableColumns(session, new SchemaTablePrefix(tableName.getSchemaName(), tableName.getTableName()))) + .extracting(TableColumnsMetadata::getTable) .doesNotContain(tableName); } } @@ -3087,8 +3104,9 @@ public void testIllegalStorageFormatDuringTableScan() // to make sure it can still be retrieved instead of throwing exception. try (Transaction transaction = newTransaction()) { ConnectorMetadata metadata = transaction.getMetadata(); - Map> allColumns = metadata.listTableColumns(newSession(), new SchemaTablePrefix(schemaTableName.getSchemaName())); - assertTrue(allColumns.containsKey(schemaTableName)); + assertThat(metadata.streamTableColumns(newSession(), new SchemaTablePrefix(schemaTableName.getSchemaName()))) + .extracting(TableColumnsMetadata::getTable) + .containsOnlyOnce(schemaTableName); } finally { dropTable(schemaTableName); diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConfig.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConfig.java index dc3e7f69990b..19eae80e3024 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConfig.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConfig.java @@ -101,7 +101,8 @@ public void testDefaults() .setDynamicFilteringProbeBlockingTimeout(new Duration(0, TimeUnit.MINUTES)) .setTimestampPrecision(HiveTimestampPrecision.DEFAULT_PRECISION) .setOptimizeSymlinkListing(true) - .setLegacyHiveViewTranslation(false)); + .setLegacyHiveViewTranslation(false) + .setIcebergCatalogName(null)); } @Test @@ -174,6 +175,7 @@ public void testExplicitPropertyMappings() .put("hive.timestamp-precision", "NANOSECONDS") .put("hive.optimize-symlink-listing", "false") .put("hive.legacy-hive-view-translation", "true") + .put("hive.iceberg-catalog-name", "iceberg") .build(); HiveConfig expected = new HiveConfig() @@ -242,7 +244,8 @@ public void testExplicitPropertyMappings() .setDynamicFilteringProbeBlockingTimeout(new Duration(10, TimeUnit.SECONDS)) .setTimestampPrecision(HiveTimestampPrecision.NANOSECONDS) .setOptimizeSymlinkListing(false) - .setLegacyHiveViewTranslation(true); + .setLegacyHiveViewTranslation(true) + .setIcebergCatalogName("iceberg"); assertFullMapping(properties, expected); } diff --git a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConnectorTest.java b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConnectorTest.java index e03d8dfea5b2..040994a3fdbc 100644 --- a/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConnectorTest.java +++ b/plugin/trino-hive/src/test/java/io/trino/plugin/hive/TestHiveConnectorTest.java @@ -3130,7 +3130,7 @@ private TableMetadata getTableMetadata(String catalog, String schema, String tab return transaction(getQueryRunner().getTransactionManager(), getQueryRunner().getAccessControl()) .readOnly() .execute(session, transactionSession -> { - Optional tableHandle = metadata.getTableHandle(transactionSession, new QualifiedObjectName(catalog, schema, tableName)); + Optional tableHandle = metadata.getOriginalTableHandle(transactionSession, new QualifiedObjectName(catalog, schema, tableName), Optional.empty()); assertTrue(tableHandle.isPresent()); return metadata.getTableMetadata(transactionSession, tableHandle.get()); }); @@ -3145,7 +3145,7 @@ private Object getHiveTableProperty(String tableName, Function { QualifiedObjectName name = new QualifiedObjectName(catalog, TPCH_SCHEMA, tableName); - TableHandle table = metadata.getTableHandle(transactionSession, name) + TableHandle table = metadata.getOriginalTableHandle(transactionSession, name, Optional.empty()) .orElseThrow(() -> new AssertionError("table not found: " + name)); table = metadata.applyFilter(transactionSession, table, Constraint.alwaysTrue()) .orElseThrow(() -> new AssertionError("applyFilter did not return a result")) @@ -7291,7 +7291,7 @@ private HiveInsertTableHandle getHiveInsertTableHandle(Session session, String t return transaction(getQueryRunner().getTransactionManager(), getQueryRunner().getAccessControl()) .execute(session, transactionSession -> { QualifiedObjectName objectName = new QualifiedObjectName(catalog, TPCH_SCHEMA, tableName); - Optional handle = metadata.getTableHandle(transactionSession, objectName); + Optional handle = metadata.getOriginalTableHandle(transactionSession, objectName, Optional.empty()); List columns = ImmutableList.copyOf(metadata.getColumnHandles(transactionSession, handle.get()).values()); InsertTableHandle insertTableHandle = metadata.beginInsert(transactionSession, handle.get(), columns); HiveInsertTableHandle hiveInsertTableHandle = (HiveInsertTableHandle) insertTableHandle.getConnectorHandle(); diff --git a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/AbstractTestIcebergConnectorTest.java b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/AbstractTestIcebergConnectorTest.java index 614e85399f71..e47a02d536ec 100644 --- a/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/AbstractTestIcebergConnectorTest.java +++ b/plugin/trino-iceberg/src/test/java/io/trino/plugin/iceberg/AbstractTestIcebergConnectorTest.java @@ -1569,7 +1569,7 @@ private void assertFilterPushdown( Metadata metadata = getQueryRunner().getMetadata(); newTransaction().execute(getSession(), session -> { - TableHandle table = metadata.getTableHandle(session, tableName) + TableHandle table = metadata.getOriginalTableHandle(session, tableName, Optional.empty()) .orElseThrow(() -> new TableNotFoundException(tableName.asSchemaTableName())); Map columns = metadata.getColumnHandles(session, table); @@ -1675,7 +1675,7 @@ private TableStatistics getTableStatistics(String tableName, Constraint constrai QualifiedObjectName qualifiedName = QualifiedObjectName.valueOf(tableName); return transaction(getQueryRunner().getTransactionManager(), getQueryRunner().getAccessControl()) .execute(getSession(), session -> { - Optional optionalHandle = metadata.getTableHandle(session, qualifiedName); + Optional optionalHandle = metadata.getOriginalTableHandle(session, qualifiedName, Optional.empty()); checkArgument(optionalHandle.isPresent(), "Could not create table handle for table %s", tableName); return metadata.getTableStatistics(session, optionalHandle.get(), constraint); }); diff --git a/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestMinimalFunctionality.java b/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestMinimalFunctionality.java index f3fde7a124c0..f6f715aae653 100644 --- a/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestMinimalFunctionality.java +++ b/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestMinimalFunctionality.java @@ -143,7 +143,7 @@ public void testStreamExists() transaction(queryRunner.getTransactionManager(), new AllowAllAccessControl()) .singleStatement() .execute(SESSION, session -> { - Optional handle = queryRunner.getServer().getMetadata().getTableHandle(session, name); + Optional handle = queryRunner.getServer().getMetadata().getOriginalTableHandle(session, name, Optional.empty()); assertTrue(handle.isPresent()); }); } diff --git a/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestRecordAccess.java b/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestRecordAccess.java index 324b697567a3..195ccf9c2a7f 100644 --- a/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestRecordAccess.java +++ b/plugin/trino-kinesis/src/test/java/io/trino/plugin/kinesis/TestRecordAccess.java @@ -153,7 +153,7 @@ public void testStreamExists() transaction(queryRunner.getTransactionManager(), new AllowAllAccessControl()) .singleStatement() .execute(SESSION, session -> { - Optional handle = queryRunner.getServer().getMetadata().getTableHandle(session, name); + Optional handle = queryRunner.getServer().getMetadata().getOriginalTableHandle(session, name, Optional.empty()); assertTrue(handle.isPresent()); }); log.info("Completed first test (access table handle)"); diff --git a/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduIntegrationDynamicFilter.java b/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduIntegrationDynamicFilter.java index 3fa517e6bb09..cb29aad2187b 100644 --- a/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduIntegrationDynamicFilter.java +++ b/plugin/trino-kudu/src/test/java/io/trino/plugin/kudu/TestKuduIntegrationDynamicFilter.java @@ -96,7 +96,7 @@ public void testIncompleteDynamicFilterTimeout() .build() .beginTransactionId(transactionId, transactionManager, new AllowAllAccessControl()); QualifiedObjectName tableName = new QualifiedObjectName("kudu", "tpch", "orders"); - Optional tableHandle = runner.getMetadata().getTableHandle(session, tableName); + Optional tableHandle = runner.getMetadata().getOriginalTableHandle(session, tableName, Optional.empty()); assertTrue(tableHandle.isPresent()); SplitSource splitSource = runner.getSplitManager() .getSplits(session, tableHandle.get(), UNGROUPED_SCHEDULING, new IncompleteDynamicFilter()); diff --git a/plugin/trino-redis/src/test/java/io/trino/plugin/redis/TestMinimalFunctionality.java b/plugin/trino-redis/src/test/java/io/trino/plugin/redis/TestMinimalFunctionality.java index b1a421865ec8..914c0c5ef399 100644 --- a/plugin/trino-redis/src/test/java/io/trino/plugin/redis/TestMinimalFunctionality.java +++ b/plugin/trino-redis/src/test/java/io/trino/plugin/redis/TestMinimalFunctionality.java @@ -104,7 +104,7 @@ public void testTableExists() transaction(queryRunner.getTransactionManager(), new AllowAllAccessControl()) .singleStatement() .execute(SESSION, session -> { - Optional handle = queryRunner.getServer().getMetadata().getTableHandle(session, name); + Optional handle = queryRunner.getServer().getMetadata().getOriginalTableHandle(session, name, Optional.empty()); assertTrue(handle.isPresent()); }); } diff --git a/testing/trino-benchmark/src/main/java/io/trino/benchmark/AbstractOperatorBenchmark.java b/testing/trino-benchmark/src/main/java/io/trino/benchmark/AbstractOperatorBenchmark.java index ca06116f77dc..1d5578bb1ef8 100644 --- a/testing/trino-benchmark/src/main/java/io/trino/benchmark/AbstractOperatorBenchmark.java +++ b/testing/trino-benchmark/src/main/java/io/trino/benchmark/AbstractOperatorBenchmark.java @@ -141,7 +141,7 @@ protected final List getColumnTypes(String tableName, String... columnName // look up the table Metadata metadata = localQueryRunner.getMetadata(); QualifiedObjectName qualifiedTableName = new QualifiedObjectName(session.getCatalog().get(), session.getSchema().get(), tableName); - TableHandle tableHandle = metadata.getTableHandle(session, qualifiedTableName) + TableHandle tableHandle = metadata.getOriginalTableHandle(session, qualifiedTableName, Optional.empty()) .orElseThrow(() -> new IllegalArgumentException(format("Table '%s' does not exist", qualifiedTableName))); Map allColumnHandles = metadata.getColumnHandles(session, tableHandle); @@ -159,7 +159,7 @@ protected final OperatorFactory createTableScanOperator(int operatorId, PlanNode // look up the table Metadata metadata = localQueryRunner.getMetadata(); QualifiedObjectName qualifiedTableName = new QualifiedObjectName(session.getCatalog().get(), session.getSchema().get(), tableName); - TableHandle tableHandle = metadata.getTableHandle(session, qualifiedTableName).orElse(null); + TableHandle tableHandle = metadata.getOriginalTableHandle(session, qualifiedTableName, Optional.empty()).orElse(null); checkArgument(tableHandle != null, "Table '%s' does not exist", qualifiedTableName); // lookup the columns diff --git a/testing/trino-benchmark/src/test/java/io/trino/benchmark/MemoryLocalQueryRunner.java b/testing/trino-benchmark/src/test/java/io/trino/benchmark/MemoryLocalQueryRunner.java index 0a58a2a2f67d..829fc7fb3d24 100644 --- a/testing/trino-benchmark/src/test/java/io/trino/benchmark/MemoryLocalQueryRunner.java +++ b/testing/trino-benchmark/src/test/java/io/trino/benchmark/MemoryLocalQueryRunner.java @@ -131,7 +131,7 @@ public void dropTable(String tableName) { Session session = localQueryRunner.getDefaultSession(); Metadata metadata = localQueryRunner.getMetadata(); - Optional tableHandle = metadata.getTableHandle(session, QualifiedObjectName.valueOf(tableName)); + Optional tableHandle = metadata.getOriginalTableHandle(session, QualifiedObjectName.valueOf(tableName), Optional.empty()); assertTrue(tableHandle.isPresent(), "Table " + tableName + " does not exist"); metadata.dropTable(session, tableHandle.get()); } diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/SinglenodeHiveRedirectionToIceberg.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/SinglenodeHiveRedirectionToIceberg.java new file mode 100644 index 000000000000..0be0c5e6d997 --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/env/environment/SinglenodeHiveRedirectionToIceberg.java @@ -0,0 +1,52 @@ +/* + * Licensed 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 io.trino.tests.product.launcher.env.environment; + +import com.google.common.collect.ImmutableList; +import io.trino.tests.product.launcher.docker.DockerFiles; +import io.trino.tests.product.launcher.env.Environment; +import io.trino.tests.product.launcher.env.EnvironmentProvider; +import io.trino.tests.product.launcher.env.common.Hadoop; +import io.trino.tests.product.launcher.env.common.Standard; +import io.trino.tests.product.launcher.env.common.TestsEnvironment; + +import javax.inject.Inject; + +import static io.trino.tests.product.launcher.env.EnvironmentContainers.COORDINATOR; +import static io.trino.tests.product.launcher.env.common.Hadoop.CONTAINER_PRESTO_HIVE_PROPERTIES; +import static io.trino.tests.product.launcher.env.common.Hadoop.CONTAINER_PRESTO_ICEBERG_PROPERTIES; +import static java.util.Objects.requireNonNull; +import static org.testcontainers.utility.MountableFile.forHostPath; + +@TestsEnvironment +public class SinglenodeHiveRedirectionToIceberg + extends EnvironmentProvider +{ + private final DockerFiles dockerFiles; + + @Inject + public SinglenodeHiveRedirectionToIceberg(DockerFiles dockerFiles, Standard standard, Hadoop hadoop) + { + super(ImmutableList.of(standard, hadoop)); + this.dockerFiles = requireNonNull(dockerFiles, "dockerFiles is null"); + } + + @Override + public void extendEnvironment(Environment.Builder builder) + { + builder.configureContainer(COORDINATOR, container -> container + .withCopyFileToContainer(forHostPath(dockerFiles.getDockerFilesHostPath("conf/environment/singlenode-hive-redirection-to-iceberg/hive.properties")), CONTAINER_PRESTO_HIVE_PROPERTIES) + .withCopyFileToContainer(forHostPath(dockerFiles.getDockerFilesHostPath("conf/environment/singlenode-hive-redirection-to-iceberg/iceberg.properties")), CONTAINER_PRESTO_ICEBERG_PROPERTIES)); + } +} diff --git a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java index 64bf3a9bc562..7c7acdc48b4d 100644 --- a/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java +++ b/testing/trino-product-tests-launcher/src/main/java/io/trino/tests/product/launcher/suite/suites/Suite7NonGeneric.java @@ -16,6 +16,7 @@ import com.google.common.collect.ImmutableList; import io.trino.tests.product.launcher.env.EnvironmentConfig; import io.trino.tests.product.launcher.env.EnvironmentDefaults; +import io.trino.tests.product.launcher.env.environment.SinglenodeHiveRedirectionToIceberg; import io.trino.tests.product.launcher.env.environment.SinglenodeKerberosHdfsImpersonationCrossRealm; import io.trino.tests.product.launcher.env.environment.SinglenodeMysql; import io.trino.tests.product.launcher.env.environment.SinglenodePostgresql; @@ -46,6 +47,7 @@ public List getTestRuns(EnvironmentConfig config) testOnEnvironment(SinglenodeSqlserver.class).withGroups("sqlserver").build(), testOnEnvironment(SinglenodeSparkHive.class).withGroups("hive_spark_bucketing").build(), testOnEnvironment(SinglenodeSparkIceberg.class).withGroups("iceberg").withExcludedGroups("storage_formats").build(), + testOnEnvironment(SinglenodeHiveRedirectionToIceberg.class).withGroups("hive_redirection_to_iceberg").build(), testOnEnvironment(SinglenodeKerberosHdfsImpersonationCrossRealm.class).withGroups("storage_formats", "cli", "hdfs_impersonation").build(), testOnEnvironment(TwoMixedHives.class).withGroups("two_hives").build(), testOnEnvironment(TwoKerberosHives.class).withGroups("two_hives").build()); diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/hive.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/hive.properties new file mode 100644 index 000000000000..91fb859f871e --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/hive.properties @@ -0,0 +1,10 @@ +connector.name=hive-hadoop2 +hive.metastore.uri=thrift://hadoop-master:9083 +hive.config.resources=/docker/presto-product-tests/conf/presto/etc/hive-default-fs-site.xml +hive.allow-add-column=true +hive.allow-drop-column=true +hive.allow-rename-column=true +hive.allow-comment-table=true +hive.allow-drop-table=true +hive.allow-rename-table=true +hive.iceberg-catalog-name=iceberg diff --git a/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/iceberg.properties b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/iceberg.properties new file mode 100644 index 000000000000..6230d550a4ac --- /dev/null +++ b/testing/trino-product-tests-launcher/src/main/resources/docker/presto-product-tests/conf/environment/singlenode-hive-redirection-to-iceberg/iceberg.properties @@ -0,0 +1,3 @@ +connector.name=iceberg +hive.metastore.uri=thrift://hadoop-master:9083 +hive.config.resources=/docker/presto-product-tests/conf/presto/etc/hive-default-fs-site.xml diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java index b8f9813b6dd3..b97db64faf76 100644 --- a/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/TestGroups.java @@ -46,6 +46,7 @@ public final class TestGroups public static final String HIVE_VIEW_COMPATIBILITY = "hive_view_compatibility"; public static final String HIVE_CACHING = "hive_caching"; public static final String HIVE_WITH_EXTERNAL_WRITES = "hive_with_external_writes"; + public static final String HIVE_REDIRECTION_TO_ICEBERG = "hive_redirection_to_iceberg"; public static final String AUTHORIZATION = "authorization"; public static final String HIVE_COERCION = "hive_coercion"; public static final String CASSANDRA = "cassandra"; diff --git a/testing/trino-product-tests/src/main/java/io/trino/tests/product/hive/TestHiveRedirectionToIceberg.java b/testing/trino-product-tests/src/main/java/io/trino/tests/product/hive/TestHiveRedirectionToIceberg.java new file mode 100644 index 000000000000..04093ca1011c --- /dev/null +++ b/testing/trino-product-tests/src/main/java/io/trino/tests/product/hive/TestHiveRedirectionToIceberg.java @@ -0,0 +1,433 @@ +/* + * Licensed 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 io.trino.tests.product.hive; + +import com.google.common.collect.ImmutableList; +import io.trino.tempto.query.QueryResult; +import org.testng.annotations.Test; + +import java.sql.Date; +import java.util.List; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.tempto.assertions.QueryAssert.Row; +import static io.trino.tempto.assertions.QueryAssert.Row.row; +import static io.trino.tempto.assertions.QueryAssert.assertThat; +import static io.trino.tests.product.TestGroups.HIVE_REDIRECTION_TO_ICEBERG; +import static io.trino.tests.product.TestGroups.PROFILE_SPECIFIC_TESTS; +import static io.trino.tests.product.hive.util.TemporaryHiveTable.randomTableSuffix; +import static io.trino.tests.product.utils.QueryExecutors.onTrino; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.testng.Assert.assertEquals; + +public class TestHiveRedirectionToIceberg + extends HiveProductTest +{ + private static final String DEFAULT_SCHEMA = "default"; + private static final String HIVE_CATALOG = "hive"; + private static final String ICEBERG_CATALOG = "iceberg"; + + private static final String CREATE_TABLE_TEMPLATE_ICEBERG = "CREATE TABLE %s (_string VARCHAR, _bigint BIGINT, _date DATE) WITH (partitioning = ARRAY['_bigint', '_date'])"; + private static final String CREATE_TABLE_TEMPLATE_HIVE = "CREATE TABLE %s (_string VARCHAR, _bigint BIGINT, _date DATE) WITH (partitioned_by = ARRAY['_bigint', '_date'])"; + private static final String INSERT_TEMPLATE = "INSERT INTO %s VALUES " + + "(NULL, NULL, NULL), " + + "('abc', 1, DATE '2020-08-04'), " + + "('abcdefghijklmnopqrstuvwxyz', 2, DATE '2020-08-04')"; + private static final List EXPECTED_ROWS = ImmutableList.builder() + .add(row(null, null, null)) + .add(row("abc", 1, Date.valueOf("2020-08-04"))) + .add(row("abcdefghijklmnopqrstuvwxyz", 2, Date.valueOf("2020-08-04"))) + .build(); + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testSelect() + { + String tableName = createOnTrinoIceberg("test_select"); + insertWithTrinoIceberg(tableName); + + assertThat(onTrinoIceberg("SELECT * FROM %s", tableName)).containsOnly(EXPECTED_ROWS); + assertThat(onTrinoHive("SELECT * FROM %s", tableName)).containsOnly(EXPECTED_ROWS); + assertThatThrownBy(() -> onTrinoHive("SELECT * FROM %s", format("\"%s$partitions\"", tableName))) + .hasMessageContaining( + "Querying system table '%s$partitions' is not supported, because the source table is redirected to '%s'", + fullName(HIVE_CATALOG, DEFAULT_SCHEMA, tableName), + fullName(ICEBERG_CATALOG, DEFAULT_SCHEMA, tableName)); + + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testDescribe() + { + String tableName = createOnTrinoIceberg("test_describe"); + assertThat(onTrinoHive("DESCRIBE %s", tableName)) + .containsOnly(ImmutableList.of( + row("_string", "varchar", "", ""), + row("_bigint", "bigint", "", ""), + row("_date", "date", "", ""))); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testShowCreateTable() + { + String tableName = createOnTrinoIceberg("test_show_create"); + QueryResult icebergShowCreate = onTrinoIceberg("SHOW CREATE TABLE %s", tableName); + QueryResult hiveShowCreate = onTrinoHive("SHOW CREATE TABLE %s", tableName); + + assertEquals( + getOnlyElement(icebergShowCreate.column(1)), + ((String) getOnlyElement(hiveShowCreate.column(1))) + // Everything except for "CREATE TABLE X" should be the same + .replace( + format("CREATE TABLE %s", fullName(HIVE_CATALOG, DEFAULT_SCHEMA, tableName)), + format("CREATE TABLE %s", fullName(ICEBERG_CATALOG, DEFAULT_SCHEMA, tableName)))); + + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testDrop() + { + String tableName = createOnTrinoIceberg("test_drop"); + assertThatThrownBy(() -> onTrinoHive("DROP TABLE %s", tableName)) + .hasMessageMatching(notSupportedError("DROP TABLE", tableName)); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testDelete() + { + String tableName = createOnTrinoIceberg("test_delete"); + assertThatThrownBy(() -> onTrinoHive("DELETE FROM %s WHERE _bigint = 2", tableName)) + .hasMessageMatching(notSupportedError("DELETE", tableName)); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testInsert() + { + String tableName = createOnTrinoIceberg("test_insert"); + assertThatThrownBy(() -> insertWithTrinoHive(tableName)) + .hasMessageMatching(notSupportedError("INSERT", tableName)); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testAlterTable() + { + String tableName = createOnTrinoIceberg("test_alter_table"); + insertWithTrinoIceberg(tableName); + + assertThatThrownBy(() -> onTrinoHive("ALTER TABLE %s RENAME TO random_schema.random_table", tableName)) + .hasMessageMatching(notSupportedError("RENAME TABLE", tableName)); + + assertThatThrownBy(() -> onTrinoHive("ALTER TABLE %s ADD COLUMN _double DOUBLE", tableName)) + .hasMessageMatching(notSupportedError("ADD COLUMN", tableName)); + assertThatThrownBy(() -> onTrinoHive("ALTER TABLE %s DROP COLUMN _string", tableName)) + .hasMessageMatching(notSupportedError("DROP COLUMN", tableName)); + assertThatThrownBy(() -> onTrinoHive("ALTER TABLE %s RENAME COLUMN _bigint TO _bi", tableName)) + .hasMessageMatching(notSupportedError("RENAME COLUMN", tableName)); + + assertThatThrownBy(() -> onTrinoHive("ALTER TABLE %s SET AUTHORIZATION user", tableName)) + .hasMessageMatching(notSupportedError("SET TABLE AUTHORIZATION", tableName)); + + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testUpdate() + { + String tableName = createOnTrinoIceberg("test_update"); + assertThatThrownBy(() -> onTrinoHive("UPDATE %s SET _string = 'big_number' WHERE _bigint > 3", tableName)) + .hasMessageMatching(notSupportedError("UPDATE", tableName)); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testPartitionUpdates() + { + String tableName = createOnTrinoIceberg("test_partition_updates"); + String errorFromHiveConnector = ".*This operation is not supported on '.*', because it is redirected to '.*'. " + + "Modification operations \\(insert, alter, drop, delete, update, comment, grant, revoke\\) are not supported when redirection is enabled."; + + assertThatThrownBy(() -> onTrino().executeQuery(format( + "CALL %s.system.create_empty_partition(" + + " schema_name => '%s', table_name => '%s'," + + " partition_columns => ARRAY['_bigint', '_date']," + + " partition_values => ARRAY['5', 'DATE 2020-08-04'])", + HIVE_CATALOG, + DEFAULT_SCHEMA, + tableName))) + .hasMessageMatching(errorFromHiveConnector); + + assertThatThrownBy(() -> onTrino().executeQuery(format( + "CALL %s.system.drop_stats(schema_name => '%s', table_name => '%s', partition_values => ARRAY[ARRAY['5', 'DATE 2020-08-04']])", + HIVE_CATALOG, + DEFAULT_SCHEMA, + tableName))) + .hasMessageMatching(errorFromHiveConnector); + + assertThatThrownBy(() -> onTrinoHive("ANALYZE %s", tableName)) + .hasMessageMatching(".*Cannot collect statistics for table '.*', because it is redirected to '.*'"); + + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testGrantRevoke() + { + String tableName = createOnTrinoIceberg("test_grant_revoke"); + assertThatThrownBy(() -> onTrinoHive("GRANT SELECT on %s TO alice", tableName)) + .hasMessageMatching(notSupportedError("GRANT", tableName)); + assertThatThrownBy(() -> onTrinoHive("REVOKE ALL PRIVILEGES ON %s FROM bob", tableName)) + .hasMessageMatching(notSupportedError("REVOKE", tableName)); + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testComments() + { + String tableName = createOnTrinoIceberg("test_comment_table"); + + String icebergComment = "this is a comment from iceberg catalog"; + onTrinoIceberg("COMMENT ON TABLE %s IS '" + icebergComment + "'", tableName); + + assertThat(onTrino().executeQuery(format("SELECT comment FROM system.metadata.table_comments WHERE catalog_name='%s'", HIVE_CATALOG))) + .contains(row(icebergComment)); + + assertThat(onTrino().executeQuery(format("SELECT comment FROM system.metadata.table_comments WHERE catalog_name='%s' and schema_name='%s'", HIVE_CATALOG, DEFAULT_SCHEMA))) + .contains(row(icebergComment)); + + // Assert the case for filters pointing to a specific table + assertThat(onTrino().executeQuery(format("SELECT comment FROM system.metadata.table_comments WHERE catalog_name='%s' and schema_name='%s' and table_name='%s'", HIVE_CATALOG, DEFAULT_SCHEMA, tableName))) + .containsOnly(row(icebergComment)); + + assertThatThrownBy(() -> onTrinoHive("COMMENT ON TABLE %s IS 'ignore'", tableName)) + .hasMessageMatching(notSupportedError("COMMENT", tableName)); + + assertThatThrownBy(() -> onTrinoHive("COMMENT ON COLUMN %s._bigint IS 'ignore'", tableName)) + .hasMessageMatching(notSupportedError("COMMENT", tableName)); + + dropWithTrinoIceberg(tableName); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testHiveBehavior() + { + // Just a sanity check to verify hive catalog behavior is unaltered, perform some operations through hive catalog. + String hiveTable = createOnTrinoHive("test_hive_behavior"); + insertWithTrinoHive(hiveTable); + String icebergTable = createOnTrinoIceberg("test_hive_behavior_iceberg"); + insertWithTrinoIceberg(icebergTable); + + assertThat(onTrinoHive("SELECT * FROM %s", hiveTable)).containsOnly(EXPECTED_ROWS); + onTrinoHive("ANALYZE %s", hiveTable); + onTrinoHive("DELETE FROM %s WHERE (_bigint is NULL OR _bigint = 2)", hiveTable); + assertThat(onTrinoHive("SELECT * FROM %s", hiveTable)).containsOnly(EXPECTED_ROWS.get(1)); + + // Verify view behavior + String hiveView = "test_hive_view" + randomTableSuffix(); + onTrino().executeQuery(format( + "CREATE VIEW %s AS SELECT * FROM (SELECT * FROM %s union all SELECT * FROM %s)", + fullName(HIVE_CATALOG, DEFAULT_SCHEMA, hiveView), + fullName(HIVE_CATALOG, DEFAULT_SCHEMA, hiveTable), + fullName(HIVE_CATALOG, DEFAULT_SCHEMA, icebergTable))); + assertThat(onTrinoHive("SELECT * FROM %s", hiveView)) + .containsOnly(ImmutableList.builder() + // From redirected icebergTable + .addAll(EXPECTED_ROWS) + // From hiveTable + .add(EXPECTED_ROWS.get(1)) + .build()); + + String icebergMaterializedView = "test_materialized_view" + randomTableSuffix(); + onTrinoIceberg("CREATE MATERIALIZED VIEW %s AS SELECT 1 value", icebergMaterializedView); + // SELECT on materialized view is not redirected to iceberg + assertThatThrownBy(() -> onTrinoHive("SELECT * FROM %s", icebergMaterializedView)) + .hasMessageContaining("View data missing prefix: "); + + assertThatThrownBy(() -> onTrinoIceberg("SELECT * FROM %s", hiveTable)) + .hasMessageContaining(format("Not an Iceberg table: %s.%s", DEFAULT_SCHEMA, hiveTable)); + + onTrinoHive("DROP VIEW %s", hiveView); + onTrinoIceberg("DROP MATERIALIZED VIEW %s", icebergMaterializedView); + dropWithTrinoIceberg(icebergTable); + dropWithTrinoHive(hiveTable); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testColumnListing() + { + String testSchema = "redirection_schema_" + randomTableSuffix(); + onTrino().executeQuery(format("CREATE SCHEMA %s.%s", ICEBERG_CATALOG, testSchema)); + + String testTable = createOnTrinoIceberg(testSchema, "test_info_schema_columns"); + + List expectedColumns = ImmutableList.builder() + .add(row(HIVE_CATALOG, testSchema, testTable, "_string")) + .add(row(HIVE_CATALOG, testSchema, testTable, "_bigint")) + .add(row(HIVE_CATALOG, testSchema, testTable, "_date")) + .build(); + + assertThat(onTrino().executeQuery(format("SELECT table_catalog, table_schema, table_name, column_name FROM %s.information_schema.columns", HIVE_CATALOG))) + .contains(expectedColumns); + assertThat(onTrino().executeQuery(format("SELECT table_cat, table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_cat='%s'", HIVE_CATALOG))) + .contains(expectedColumns); + + assertThat(onTrino().executeQuery(format("SELECT table_catalog, table_schema, table_name, column_name FROM %s.information_schema.columns WHERE table_schema = '%s'", HIVE_CATALOG, testSchema))) + .containsOnly(expectedColumns); + assertThat(onTrino().executeQuery(format("SELECT table_cat, table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_cat='%s' AND table_schem='%s'", HIVE_CATALOG, testSchema))) + .containsOnly(expectedColumns); + + assertThat(onTrino().executeQuery(format( + "SELECT table_catalog, table_schema, table_name, column_name" + + " FROM %s.information_schema.columns" + + " WHERE table_schema = '%s' AND table_name = '%s'", + HIVE_CATALOG, + testSchema, + testTable))) + .containsOnly(expectedColumns); + assertThat(onTrino().executeQuery(format( + "SELECT table_cat, table_schem, table_name, column_name" + + " FROM system.jdbc.columns" + + " WHERE table_cat='%s' AND table_schem='%s' AND table_name = '%s'", + HIVE_CATALOG, + testSchema, + testTable))) + .containsOnly(expectedColumns); + assertThat(onTrino().executeQuery(format("SHOW COLUMNS FROM %s", fullName(HIVE_CATALOG, testSchema, testTable))).project(1)) + .containsOnly(row("_string"), row("_bigint"), row("_date")); + + dropWithTrinoIceberg(testSchema, testTable); + onTrino().executeQuery(format("DROP SCHEMA %s.%s", ICEBERG_CATALOG, testSchema)); + } + + @Test(groups = {HIVE_REDIRECTION_TO_ICEBERG, PROFILE_SPECIFIC_TESTS}) + public void testTableListing() + { + String testSchema = "redirection_schema_" + randomTableSuffix(); + onTrino().executeQuery(format("CREATE SCHEMA %s.%s", ICEBERG_CATALOG, testSchema)); + String testTable = createOnTrinoIceberg(testSchema, "test_info_schema_tables"); + + List expectedTableEntry = ImmutableList.of(row(HIVE_CATALOG, testSchema, testTable)); + + assertThat(onTrino().executeQuery(format("SELECT table_catalog, table_schema, table_name FROM %s.information_schema.tables", HIVE_CATALOG))) + .contains(expectedTableEntry); + assertThat(onTrino().executeQuery(format("SELECT table_cat, table_schem, table_name FROM system.jdbc.tables WHERE table_cat='%s'", HIVE_CATALOG))) + .contains(expectedTableEntry); + + assertThat(onTrino().executeQuery(format("SELECT table_catalog, table_schema, table_name FROM %s.information_schema.tables WHERE table_schema = '%s'", HIVE_CATALOG, testSchema))) + .containsOnly(expectedTableEntry); + assertThat(onTrino().executeQuery(format("SELECT table_cat, table_schem, table_name FROM system.jdbc.tables WHERE table_cat='%s' AND table_schem='%s'", HIVE_CATALOG, testSchema))) + .containsOnly(expectedTableEntry); + assertThat(onTrino().executeQuery(format("SHOW TABLES FROM %s.%s", HIVE_CATALOG, testSchema))) + .containsOnly(row(testTable)); + + // Assert the case for filters pointing to a specific table + assertThat(onTrino().executeQuery(format( + "SELECT table_catalog, table_schema, table_name" + + " FROM %s.information_schema.tables" + + " WHERE table_schema = '%s' AND table_name = '%s'", + HIVE_CATALOG, + testSchema, + testTable))) + .containsOnly(expectedTableEntry); + assertThat(onTrino().executeQuery(format( + "SELECT table_cat, table_schem, table_name" + + " FROM system.jdbc.tables" + + " WHERE table_cat='%s' AND table_schem='%s' AND table_name = '%s'", + HIVE_CATALOG, + testSchema, + testTable))) + .containsOnly(expectedTableEntry); + + dropWithTrinoIceberg(testSchema, testTable); + onTrino().executeQuery(format("DROP SCHEMA %s.%s", ICEBERG_CATALOG, testSchema)); + } + + private static QueryResult onTrinoIceberg(String queryTemplate, String tableName) + { + return onTrinoIceberg(queryTemplate, DEFAULT_SCHEMA, tableName); + } + + private static QueryResult onTrinoIceberg(String queryTemplate, String schemaName, String tableName) + { + return onTrino().executeQuery(format(queryTemplate, fullName(ICEBERG_CATALOG, schemaName, tableName))); + } + + private static String createOnTrinoIceberg(String tableNamePrefix) + { + return createOnTrinoIceberg(DEFAULT_SCHEMA, tableNamePrefix); + } + + private static String createOnTrinoIceberg(String schemaName, String tableNamePrefix) + { + String tableName = tableNamePrefix + randomTableSuffix(); + onTrinoIceberg(CREATE_TABLE_TEMPLATE_ICEBERG, schemaName, tableName); + return tableName; + } + + private static void insertWithTrinoIceberg(String tableName) + { + onTrinoIceberg(INSERT_TEMPLATE, DEFAULT_SCHEMA, tableName); + } + + private static void dropWithTrinoIceberg(String schemaName, String tableName) + { + onTrinoIceberg("DROP TABLE %s", schemaName, tableName); + } + + private static void dropWithTrinoIceberg(String tableName) + { + dropWithTrinoIceberg(DEFAULT_SCHEMA, tableName); + } + + private static QueryResult onTrinoHive(String queryTemplate, String tableName) + { + return onTrino().executeQuery(format(queryTemplate, fullName(HIVE_CATALOG, DEFAULT_SCHEMA, tableName))); + } + + private static String createOnTrinoHive(String tableNamePrefix) + { + String tableName = tableNamePrefix + randomTableSuffix(); + onTrinoHive(CREATE_TABLE_TEMPLATE_HIVE, tableName); + return tableName; + } + + private static void insertWithTrinoHive(String tableName) + { + onTrinoHive(INSERT_TEMPLATE, tableName); + } + + private static void dropWithTrinoHive(String tableName) + { + onTrinoHive("DROP TABLE %s", tableName); + } + + private static String fullName(String catalogName, String schemaName, String tableName) + { + return format("%s.%s.%s", catalogName, schemaName, tableName); + } + + private static final String notSupportedError(String operation, String tableName) + { + return format( + ".*Failed to perform operation '%s'. It is not supported on table '%s' because the table is redirected to '%s'", + operation, + fullName(HIVE_CATALOG, DEFAULT_SCHEMA, tableName), + fullName(ICEBERG_CATALOG, DEFAULT_SCHEMA, tableName)); + } +} diff --git a/testing/trino-tests/src/test/java/io/trino/execution/TestTableRedirection.java b/testing/trino-tests/src/test/java/io/trino/execution/TestTableRedirection.java index 32231a3e4f56..ef3914236c2d 100644 --- a/testing/trino-tests/src/test/java/io/trino/execution/TestTableRedirection.java +++ b/testing/trino-tests/src/test/java/io/trino/execution/TestTableRedirection.java @@ -269,6 +269,15 @@ public void testTableListing() .map(tableName -> row(mappings.getKey(), tableName))) .flatMap(Function.identity()) .collect(Collectors.joining(",")))); + + assertQuery( + format("SELECT table_schema, table_name" + + " FROM information_schema.tables" + + " WHERE table_catalog='%s' AND table_schema = '%s' AND table_name='%s'", + CATALOG_NAME, + SCHEMA_ONE, + VALID_REDIRECTION_SRC), + format("VALUES ('%s', '%s')", SCHEMA_ONE, VALID_REDIRECTION_SRC)); } @Test @@ -363,6 +372,52 @@ public void testShowColumns() .hasMessageContaining("Table redirections form a loop"); } + @Test + public void testUnsupportedOperations() + { + CatalogSchemaTableName source = new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, VALID_REDIRECTION_SRC); + CatalogSchemaTableName target = new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, VALID_REDIRECTION_TARGET); + + assertThatThrownBy(() -> query(format("COMMENT ON TABLE %s IS 'ignore'", source))) + .hasMessageContaining(notSupportedError("COMMENT", source, target)); + assertThatThrownBy(() -> query(format("COMMENT ON COLUMN %s.%s IS 'ignore'", source, C0))) + .hasMessageContaining(notSupportedError("COMMENT", source, target)); + + assertThatThrownBy(() -> query(format("DROP TABLE %s", source))) + .hasMessageContaining(notSupportedError("DROP TABLE", source, target)); + + assertThatThrownBy(() -> query(format("DELETE FROM %s WHERE %s = 2", source, C0))) + .hasMessageContaining(notSupportedError("DELETE", source, target)); + + assertThatThrownBy(() -> query(format("INSERT INTO %s SELECT * FROM %s", source, target))) + .hasMessageContaining(notSupportedError("INSERT", source, target)); + + assertThatThrownBy(() -> query(format("ALTER TABLE %s RENAME TO %s", source, new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, "ignore")))) + .hasMessageContaining(notSupportedError("RENAME TABLE", source, target)); + + assertThatThrownBy(() -> query(format("ALTER TABLE %s ADD COLUMN ignore BIGINT", source))) + .hasMessageContaining(notSupportedError("ADD COLUMN", source, target)); + assertThatThrownBy(() -> query(format("ALTER TABLE %s DROP COLUMN %s", source, C0))) + .hasMessageContaining(notSupportedError("DROP COLUMN", source, target)); + assertThatThrownBy(() -> query(format("ALTER TABLE %s RENAME COLUMN %s TO ignore", source, C0))) + .hasMessageContaining(notSupportedError("RENAME COLUMN", source, target)); + + assertThatThrownBy(() -> query(format("ALTER TABLE %s SET AUTHORIZATION user", source))) + .hasMessageContaining(notSupportedError("SET TABLE AUTHORIZATION", source, target)); + assertThatThrownBy(() -> query(format("UPDATE %s SET %s = 2 WHERE %s > 3", source, C0, C1))) + .hasMessageContaining(notSupportedError("UPDATE", source, target)); + + assertThatThrownBy(() -> query(format("GRANT SELECT on %s TO alice", source))) + .hasMessageMatching(notSupportedError("GRANT", source, target)); + assertThatThrownBy(() -> query(format("REVOKE ALL PRIVILEGES ON %s FROM bob", source))) + .hasMessageMatching(notSupportedError("REVOKE", source, target)); + } + + private static final String notSupportedError(String operation, CatalogSchemaTableName source, CatalogSchemaTableName target) + { + return format("Failed to perform operation '%s'. It is not supported on table '%s' because the table is redirected to '%s'", operation, source, target); + } + // TODO: Add tests for redirection in CommentsSystemTable and CREATE TABLE LIKE private static String row(String... values)