diff --git a/core/trino-main/src/main/java/io/trino/connector/informationschema/InformationSchemaMetadata.java b/core/trino-main/src/main/java/io/trino/connector/informationschema/InformationSchemaMetadata.java index 1c78f2d1efc3..ea271c36f4ae 100644 --- a/core/trino-main/src/main/java/io/trino/connector/informationschema/InformationSchemaMetadata.java +++ b/core/trino-main/src/main/java/io/trino/connector/informationschema/InformationSchemaMetadata.java @@ -22,6 +22,7 @@ import io.trino.metadata.Metadata; import io.trino.metadata.QualifiedObjectName; import io.trino.metadata.QualifiedTablePrefix; +import io.trino.spi.TrinoException; import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.ConnectorMetadata; @@ -65,6 +66,7 @@ import static io.trino.connector.informationschema.InformationSchemaTable.TABLE_PRIVILEGES; import static io.trino.connector.informationschema.InformationSchemaTable.VIEWS; import static io.trino.metadata.MetadataUtil.findColumnMetadata; +import static io.trino.spi.StandardErrorCode.TABLE_REDIRECTION_ERROR; import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; import static java.util.Collections.emptyList; import static java.util.Locale.ENGLISH; @@ -353,7 +355,27 @@ private Set calculatePrefixesWithTableName( .flatMap(prefix -> tables.get().stream() .filter(this::isLowerCase) .map(table -> new QualifiedObjectName(catalogName, prefix.getSchemaName().get(), table))) - .filter(objectName -> !isColumnsEnumeratingTable(informationSchemaTable) || metadata.getTableHandle(session, objectName).isPresent() || metadata.getView(session, objectName).isPresent()) + .filter(objectName -> { + if (!isColumnsEnumeratingTable(informationSchemaTable) || metadata.getView(session, objectName).isPresent()) { + return true; + } + + // This is a columns enumerating table and the object is not a view + try { + // Table redirection to enumerate columns from target table happens later in + // MetadataListing#listTableColumns, but also applying it here to avoid incorrect + // filtering in case the source table does not exist or there is a problem with redirection. + return metadata.getRedirectionAwareTableHandle(session, objectName).getTableHandle().isPresent(); + } + catch (TrinoException e) { + if (e.getErrorCode().equals(TABLE_REDIRECTION_ERROR.toErrorCode())) { + // Ignore redirection errors for listing, treat as if the table does not exist + return false; + } + + throw e; + } + }) .filter(objectName -> predicate.isEmpty() || predicate.get().test(asFixedValues(objectName))) .map(QualifiedObjectName::asQualifiedTablePrefix) .collect(toImmutableSet()); diff --git a/core/trino-main/src/main/java/io/trino/connector/system/TableCommentSystemTable.java b/core/trino-main/src/main/java/io/trino/connector/system/TableCommentSystemTable.java index 90d1ac9737df..0a803585d6a5 100644 --- a/core/trino-main/src/main/java/io/trino/connector/system/TableCommentSystemTable.java +++ b/core/trino-main/src/main/java/io/trino/connector/system/TableCommentSystemTable.java @@ -108,7 +108,7 @@ public RecordCursor cursor(ConnectorTransactionHandle transactionHandle, Connect QualifiedObjectName tableName = new QualifiedObjectName(prefix.getCatalogName(), name.getSchemaName(), name.getTableName()); Optional comment = Optional.empty(); try { - comment = metadata.getTableHandle(session, tableName) + comment = metadata.getRedirectionAwareTableHandle(session, tableName).getTableHandle() .map(handle -> metadata.getTableMetadata(session, handle)) .map(metadata -> metadata.getMetadata().getComment()) .get(); 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 0ea34af2dcf4..7650f01e1ef4 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 @@ -23,6 +23,7 @@ import io.trino.execution.warnings.WarningCollector; import io.trino.metadata.Metadata; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.TableHandle; import io.trino.metadata.TableMetadata; import io.trino.security.AccessControl; @@ -75,6 +76,7 @@ import static io.trino.sql.tree.LikeClause.PropertiesOption.EXCLUDING; import static io.trino.sql.tree.LikeClause.PropertiesOption.INCLUDING; import static io.trino.type.UnknownType.UNKNOWN; +import static java.lang.String.format; public class CreateTableTask implements DataDefinitionTask @@ -166,15 +168,23 @@ ListenableFuture internalExecute(CreateTable statement, Metadata metadata, Ac } else if (element instanceof LikeClause) { LikeClause likeClause = (LikeClause) element; - QualifiedObjectName likeTableName = createQualifiedObjectName(session, statement, likeClause.getTableName()); - if (metadata.getCatalogHandle(session, likeTableName.getCatalogName()).isEmpty()) { - throw semanticException(CATALOG_NOT_FOUND, statement, "LIKE table catalog '%s' does not exist", likeTableName.getCatalogName()); + QualifiedObjectName originalLikeTableName = createQualifiedObjectName(session, statement, likeClause.getTableName()); + if (metadata.getCatalogHandle(session, originalLikeTableName.getCatalogName()).isEmpty()) { + throw semanticException(CATALOG_NOT_FOUND, statement, "LIKE table catalog '%s' does not exist", originalLikeTableName.getCatalogName()); } + + RedirectionAwareTableHandle redirection = metadata.getRedirectionAwareTableHandle(session, originalLikeTableName); + TableHandle likeTable = redirection.getTableHandle() + .orElseThrow(() -> semanticException(TABLE_NOT_FOUND, statement, "LIKE table '%s' does not exist", originalLikeTableName)); + + QualifiedObjectName likeTableName = redirection.getRedirectedTableName().orElse(originalLikeTableName); if (!tableName.getCatalogName().equals(likeTableName.getCatalogName())) { - throw semanticException(NOT_SUPPORTED, statement, "LIKE table across catalogs is not supported"); + String message = "CREATE TABLE LIKE across catalogs is not supported"; + if (!originalLikeTableName.equals(likeTableName)) { + message += format(". LIKE table '%s' redirected to '%s'.", originalLikeTableName, likeTableName); + } + throw semanticException(NOT_SUPPORTED, statement, message); } - TableHandle likeTable = metadata.getTableHandle(session, likeTableName) - .orElseThrow(() -> semanticException(TABLE_NOT_FOUND, statement, "LIKE table '%s' does not exist", likeTableName)); TableMetadata likeTableMetadata = metadata.getTableMetadata(session, likeTable); 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 15b098f50dd9..800b9d0bcb74 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 @@ -43,6 +43,7 @@ import io.trino.spi.connector.SampleType; import io.trino.spi.connector.SortItem; import io.trino.spi.connector.SystemTable; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.expression.ConnectorExpression; @@ -157,9 +158,10 @@ public interface Metadata ColumnMetadata getColumnMetadata(Session session, TableHandle tableHandle, ColumnHandle columnHandle); /** - * Gets the metadata for all columns that match the specified table prefix. + * Gets the columns metadata for all tables that match the specified prefix. + * TODO: consider returning a stream for more efficient processing */ - Map> listTableColumns(Session session, QualifiedTablePrefix prefix); + Map> listTableColumns(Session session, QualifiedTablePrefix prefix); /** * Creates a schema. @@ -640,4 +642,10 @@ default ResolvedFunction getCoercion(Type fromType, Type toType) * This method is called after security checks against the original table. */ Optional applyTableScanRedirect(Session session, TableHandle tableHandle); + + /** + * Get the target table handle after performing redirection. + * + */ + RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName); } diff --git a/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java b/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java index 8af6d86b24c3..c49ba9b946d0 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java +++ b/core/trino-main/src/main/java/io/trino/metadata/MetadataListing.java @@ -20,10 +20,11 @@ import io.trino.Session; import io.trino.connector.CatalogName; import io.trino.security.AccessControl; -import io.trino.spi.connector.CatalogSchemaTableName; +import io.trino.spi.TrinoException; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.ConnectorViewDefinition; import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.security.GrantInfo; import java.util.List; @@ -37,6 +38,8 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.trino.spi.StandardErrorCode.TABLE_REDIRECTION_ERROR; public final class MetadataListing { @@ -132,31 +135,75 @@ public static Set listTablePrivileges(Session session, Metadata metad public static Map> listTableColumns(Session session, Metadata metadata, AccessControl accessControl, QualifiedTablePrefix prefix) { - Map> tableColumns = metadata.listTableColumns(session, prefix).entrySet().stream() - .collect(toImmutableMap(entry -> entry.getKey().asSchemaTableName(), Entry::getValue)); + List catalogColumns = getOnlyElement(metadata.listTableColumns(session, prefix).values(), List.of()); + + Map>> tableColumns = catalogColumns.stream() + .collect(toImmutableMap(TableColumnsMetadata::getTable, TableColumnsMetadata::getColumns)); + Set allowedTables = accessControl.filterTables( session.toSecurityContext(), prefix.getCatalogName(), tableColumns.keySet()); ImmutableMap.Builder> result = ImmutableMap.builder(); - for (Entry> entry : tableColumns.entrySet()) { - if (!allowedTables.contains(entry.getKey())) { - continue; + + tableColumns.forEach((table, columnsOptional) -> { + if (!allowedTables.contains(table)) { + return; } - List columns = entry.getValue(); + + QualifiedObjectName originalTableName = new QualifiedObjectName(prefix.getCatalogName(), table.getSchemaName(), table.getTableName()); + List columns; + Optional targetTableName = Optional.empty(); + + if (columnsOptional.isPresent()) { + columns = columnsOptional.get(); + } + else { + TableHandle targetTableHandle = null; + boolean redirectionSucceeded = false; + + try { + // Handle redirection before filterColumns check + RedirectionAwareTableHandle redirection = metadata.getRedirectionAwareTableHandle(session, originalTableName); + targetTableName = redirection.getRedirectedTableName(); + + // The target table name should be non-empty. If it is empty, it means that there is an + // inconsistency in the connector's implementation of ConnectorMetadata#streamTableColumns and + // ConnectorMetadata#redirectTable. + if (targetTableName.isPresent()) { + redirectionSucceeded = true; + targetTableHandle = redirection.getTableHandle().orElseThrow(); + } + } + catch (TrinoException e) { + // Ignore redirection errors + if (!e.getErrorCode().equals(TABLE_REDIRECTION_ERROR.toErrorCode())) { + throw e; + } + } + + if (redirectionSucceeded == false) { + return; + } + + columns = metadata.getTableMetadata(session, targetTableHandle).getColumns(); + } + Set allowedColumns = accessControl.filterColumns( session.toSecurityContext(), - new CatalogSchemaTableName(prefix.getCatalogName(), entry.getKey()), + // Use redirected table name for applying column filters + targetTableName.orElse(originalTableName).asCatalogSchemaTableName(), columns.stream() .map(ColumnMetadata::getName) .collect(toImmutableSet())); result.put( - entry.getKey(), + table, columns.stream() .filter(column -> allowedColumns.contains(column.getName())) .collect(toImmutableList())); - } + }); + return result.build(); } } 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 0b4fea4c0133..55607f4226c4 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 @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; +import com.google.common.collect.Streams; import com.google.common.util.concurrent.UncheckedExecutionException; import io.airlift.slice.Slice; import io.trino.Session; @@ -90,6 +91,7 @@ import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortItem; import io.trino.spi.connector.SystemTable; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.expression.ConnectorExpression; @@ -143,6 +145,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; @@ -154,6 +157,8 @@ import static com.google.common.primitives.Primitives.wrap; import static io.trino.metadata.FunctionKind.AGGREGATE; import static io.trino.metadata.QualifiedObjectName.convertFromSchemaTableName; +import static io.trino.metadata.RedirectionAwareTableHandle.noRedirection; +import static io.trino.metadata.RedirectionAwareTableHandle.withRedirectionTo; import static io.trino.metadata.Signature.mangleOperatorName; import static io.trino.metadata.SignatureBinder.applyBoundVariables; import static io.trino.spi.StandardErrorCode.FUNCTION_IMPLEMENTATION_ERROR; @@ -163,6 +168,7 @@ import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.StandardErrorCode.SCHEMA_NOT_FOUND; import static io.trino.spi.StandardErrorCode.SYNTAX_ERROR; +import static io.trino.spi.StandardErrorCode.TABLE_REDIRECTION_ERROR; import static io.trino.spi.connector.ConnectorViewDefinition.ViewColumn; import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.BOXED_NULLABLE; import static io.trino.spi.function.InvocationConvention.InvocationArgumentConvention.NEVER_NULL; @@ -189,6 +195,9 @@ public final class MetadataManager implements Metadata { + @VisibleForTesting + public static final int MAX_TABLE_REDIRECTIONS = 10; + private final FunctionRegistry functions; private final TypeOperators typeOperators; private final FunctionResolver functionResolver; @@ -633,12 +642,14 @@ private boolean isExistingRelation(Session session, QualifiedObjectName name) } @Override - public Map> listTableColumns(Session session, QualifiedTablePrefix prefix) + public Map> listTableColumns(Session session, QualifiedTablePrefix prefix) { requireNonNull(prefix, "prefix is null"); Optional catalog = getOptionalCatalogMetadata(session, prefix.getCatalogName()); - Map> tableColumns = new HashMap<>(); + + // Track column metadata for every object name to resolve ties between table and view + Map>> tableColumns = new HashMap<>(); if (catalog.isPresent()) { CatalogMetadata catalogMetadata = catalog.get(); @@ -647,15 +658,12 @@ public Map> listTableColumns(Session s ConnectorMetadata metadata = catalogMetadata.getMetadataFor(catalogName); ConnectorSession connectorSession = session.toConnectorSession(catalogName); - for (Entry> entry : metadata.listTableColumns(connectorSession, tablePrefix).entrySet()) { - QualifiedObjectName tableName = new QualifiedObjectName( - prefix.getCatalogName(), - entry.getKey().getSchemaName(), - entry.getKey().getTableName()); - tableColumns.put(tableName, entry.getValue()); - } - // if table and view names overlap, the view wins + // Collect column metadata from tables + metadata.streamTableColumns(connectorSession, tablePrefix) + .forEach(columnsMetadata -> tableColumns.put(columnsMetadata.getTable(), columnsMetadata.getColumns())); + + // Collect column metadata from views. if table and view names overlap, the view wins for (Entry entry : getViews(session, prefix).entrySet()) { ImmutableList.Builder columns = ImmutableList.builder(); for (ViewColumn column : entry.getValue().getColumns()) { @@ -666,11 +674,15 @@ public Map> listTableColumns(Session s throw new TrinoException(INVALID_VIEW, format("Unknown type '%s' for column '%s' in view: %s", column.getType(), column.getName(), entry.getKey())); } } - tableColumns.put(entry.getKey(), columns.build()); + tableColumns.put(entry.getKey().asSchemaTableName(), Optional.of(columns.build())); } } } - return ImmutableMap.copyOf(tableColumns); + return ImmutableMap.of( + new CatalogName(prefix.getCatalogName()), + tableColumns.entrySet().stream() + .map(entry -> new TableColumnsMetadata(entry.getKey(), entry.getValue())) + .collect(toImmutableList())); } @Override @@ -1284,6 +1296,71 @@ public Optional applyTableScanRedirect(Sessi return metadata.applyTableScanRedirect(connectorSession, tableHandle.getConnectorHandle()); } + private QualifiedObjectName getRedirectedTableName(Session session, QualifiedObjectName originalTableName) + { + requireNonNull(session, "session is null"); + requireNonNull(originalTableName, "originalTableName is null"); + + QualifiedObjectName tableName = originalTableName; + Set visitedTableNames = new LinkedHashSet<>(); + visitedTableNames.add(tableName); + + for (int count = 0; count < MAX_TABLE_REDIRECTIONS; count++) { + Optional catalog = getOptionalCatalogMetadata(session, tableName.getCatalogName()); + + if (catalog.isEmpty()) { + // Stop redirection + return tableName; + } + + CatalogMetadata catalogMetadata = catalog.get(); + CatalogName catalogName = catalogMetadata.getConnectorId(session, tableName); + ConnectorMetadata metadata = catalogMetadata.getMetadataFor(catalogName); + + Optional redirectedTableName = metadata.redirectTable(session.toConnectorSession(catalogName), tableName.asSchemaTableName()) + .map(name -> convertFromSchemaTableName(name.getCatalogName()).apply(name.getSchemaTableName())); + + if (redirectedTableName.isEmpty()) { + return tableName; + } + + tableName = redirectedTableName.get(); + + // Check for loop in redirection + if (!visitedTableNames.add(tableName)) { + throw new TrinoException(TABLE_REDIRECTION_ERROR, + format("Table redirections form a loop: %s", + Streams.concat(visitedTableNames.stream(), Stream.of(tableName)) + .map(QualifiedObjectName::toString) + .collect(Collectors.joining(" -> ")))); + } + } + throw new TrinoException(TABLE_REDIRECTION_ERROR, format("Table redirected too many times (%d): %s", MAX_TABLE_REDIRECTIONS, visitedTableNames)); + } + + @Override + public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName) + { + QualifiedObjectName targetTableName = getRedirectedTableName(session, tableName); + if (targetTableName.equals(tableName)) { + return noRedirection(getTableHandle(session, tableName)); + } + + Optional tableHandle = getTableHandle(session, targetTableName); + if (tableHandle.isPresent()) { + return withRedirectionTo(targetTableName, tableHandle.get()); + } + + // Redirected table must exist + if (getCatalogHandle(session, targetTableName.getCatalogName()).isEmpty()) { + throw new TrinoException(TABLE_REDIRECTION_ERROR, format("Table '%s' redirected to '%s', but the target catalog '%s' does not exist", tableName, targetTableName, targetTableName.getCatalogName())); + } + if (!schemaExists(session, new CatalogSchemaName(targetTableName.getCatalogName(), targetTableName.getSchemaName()))) { + throw new TrinoException(TABLE_REDIRECTION_ERROR, format("Table '%s' redirected to '%s', but the target schema '%s' does not exist", tableName, targetTableName, targetTableName.getSchemaName())); + } + 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 resolveIndex(Session session, TableHandle tableHandle, Set indexableColumns, Set outputColumns, TupleDomain tupleDomain) { @@ -1673,6 +1750,7 @@ public void revokeSchemaPrivileges(Session session, CatalogSchemaName schemaName metadata.revokeSchemaPrivileges(session.toConnectorSession(catalogName), schemaName.getSchemaName(), privileges, grantee, grantOption); } + // TODO support table redirection @Override public List listTablePrivileges(Session session, QualifiedTablePrefix prefix) { diff --git a/core/trino-main/src/main/java/io/trino/metadata/RedirectionAwareTableHandle.java b/core/trino-main/src/main/java/io/trino/metadata/RedirectionAwareTableHandle.java new file mode 100644 index 000000000000..8b524a4b6977 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/metadata/RedirectionAwareTableHandle.java @@ -0,0 +1,82 @@ +/* + * 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.metadata; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public abstract class RedirectionAwareTableHandle +{ + private final Optional tableHandle; + + protected RedirectionAwareTableHandle(Optional tableHandle) + { + this.tableHandle = requireNonNull(tableHandle, "tableHandle is null"); + } + + public static RedirectionAwareTableHandle withRedirectionTo(QualifiedObjectName redirectedTableName, TableHandle tableHandle) + { + return new TableHandleWithRedirection(redirectedTableName, tableHandle); + } + + public static RedirectionAwareTableHandle noRedirection(Optional tableHandle) + { + return new TableHandleWithoutRedirection(tableHandle); + } + + public Optional getTableHandle() + { + return tableHandle; + } + + /** + * @return the target table name after redirection. Optional.empty() if the table is not redirected. + */ + public abstract Optional getRedirectedTableName(); + + private static class TableHandleWithoutRedirection + extends RedirectionAwareTableHandle + { + protected TableHandleWithoutRedirection(Optional tableHandle) + { + super(tableHandle); + } + + @Override + public Optional getRedirectedTableName() + { + return Optional.empty(); + } + } + + private static class TableHandleWithRedirection + extends RedirectionAwareTableHandle + { + private final QualifiedObjectName redirectedTableName; + + public TableHandleWithRedirection(QualifiedObjectName redirectedTableName, TableHandle tableHandle) + { + // Table handle must exist if there is redirection + super(Optional.of(tableHandle)); + this.redirectedTableName = requireNonNull(redirectedTableName, "redirectedTableName is null"); + } + + @Override + public Optional getRedirectedTableName() + { + return Optional.of(redirectedTableName); + } + } +} 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 e3e97032d183..b9b20b5c557a 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 @@ -32,6 +32,7 @@ import io.trino.metadata.NewTableLayout; import io.trino.metadata.OperatorNotFoundException; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.ResolvedFunction; import io.trino.metadata.TableHandle; import io.trino.metadata.TableMetadata; @@ -537,6 +538,7 @@ 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); @@ -1280,7 +1282,6 @@ protected Scope visitTable(Table table, Optional scope) } QualifiedObjectName name = createQualifiedObjectName(session, table, table.getName()); - analysis.addEmptyColumnReferencesForTable(accessControl, session.getIdentity(), name); Optional optionalMaterializedView = metadata.getMaterializedView(session, name); if (optionalMaterializedView.isPresent()) { @@ -1291,6 +1292,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); if (tableHandle.isEmpty()) { throw semanticException(INVALID_VIEW, table, "Storage table '%s' does not exist", storageTableName); @@ -1307,24 +1309,30 @@ protected Scope visitTable(Table table, Optional scope) // This could be a reference to a logical view or a table Optional optionalView = metadata.getView(session, name); if (optionalView.isPresent()) { + analysis.addEmptyColumnReferencesForTable(accessControl, session.getIdentity(), name); return createScopeForView(table, name, scope, optionalView.get()); } - Optional tableHandle = metadata.getTableHandle(session, name); + + // This can only be a table + RedirectionAwareTableHandle redirection = metadata.getRedirectionAwareTableHandle(session, name); + Optional tableHandle = redirection.getTableHandle(); + QualifiedObjectName targetTableName = redirection.getRedirectedTableName().orElse(name); + analysis.addEmptyColumnReferencesForTable(accessControl, session.getIdentity(), targetTableName); if (tableHandle.isEmpty()) { - if (metadata.getCatalogHandle(session, name.getCatalogName()).isEmpty()) { - throw semanticException(CATALOG_NOT_FOUND, table, "Catalog '%s' does not exist", name.getCatalogName()); + if (metadata.getCatalogHandle(session, targetTableName.getCatalogName()).isEmpty()) { + throw semanticException(CATALOG_NOT_FOUND, table, "Catalog '%s' does not exist", targetTableName.getCatalogName()); } - if (!metadata.schemaExists(session, new CatalogSchemaName(name.getCatalogName(), name.getSchemaName()))) { - throw semanticException(SCHEMA_NOT_FOUND, table, "Schema '%s' does not exist", name.getSchemaName()); + if (!metadata.schemaExists(session, new CatalogSchemaName(targetTableName.getCatalogName(), targetTableName.getSchemaName()))) { + throw semanticException(SCHEMA_NOT_FOUND, table, "Schema '%s' does not exist", targetTableName.getSchemaName()); } - throw semanticException(TABLE_NOT_FOUND, table, "Table '%s' does not exist", name); + throw semanticException(TABLE_NOT_FOUND, table, "Table '%s' does not exist", targetTableName); } TableSchema tableSchema = metadata.getTableSchema(session, tableHandle.get()); Map columnHandles = metadata.getColumnHandles(session, tableHandle.get()); ImmutableList.Builder fields = ImmutableList.builder(); - fields.addAll(analyzeTableOutputFields(table, tableSchema, columnHandles)); + fields.addAll(analyzeTableOutputFields(table, targetTableName, tableSchema, columnHandles)); if (updateKind.isPresent()) { // Add the row id field @@ -1355,7 +1363,7 @@ protected Scope visitTable(Table table, Optional scope) List outputFields = fields.build(); - analyzeFiltersAndMasks(table, name, tableHandle, outputFields, session.getIdentity().getUser()); + analyzeFiltersAndMasks(table, targetTableName, tableHandle, outputFields, session.getIdentity().getUser()); Scope tableScope = createAndAssignScope(table, scope, outputFields); @@ -1368,6 +1376,13 @@ protected Scope visitTable(Table table, Optional scope) return tableScope; } + private void checkStorageTableNotRedirected(QualifiedObjectName source) + { + 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)); + }); + } + private void analyzeFiltersAndMasks(Table table, QualifiedObjectName name, Optional tableHandle, List fields, String authorization) { Scope accessControlScope = Scope.builder() @@ -1557,7 +1572,9 @@ private List analyzeStorageTable(Table table, List viewFields, Tab { TableSchema tableSchema = metadata.getTableSchema(session, storageTable); Map columnHandles = metadata.getColumnHandles(session, storageTable); - List tableFields = analyzeTableOutputFields(table, tableSchema, columnHandles) + QualifiedObjectName tableName = createQualifiedObjectName(session, table, table.getName()); + checkStorageTableNotRedirected(tableName); + List tableFields = analyzeTableOutputFields(table, tableName, tableSchema, columnHandles) .stream() .filter(field -> !field.isHidden()) .collect(toImmutableList()); @@ -1615,10 +1632,9 @@ private List analyzeStorageTable(Table table, List viewFields, Tab return tableFields; } - private List analyzeTableOutputFields(Table table, TableSchema tableSchema, Map columnHandles) + private List analyzeTableOutputFields(Table table, QualifiedObjectName tableName, TableSchema tableSchema, Map columnHandles) { // TODO: discover columns lazily based on where they are needed (to support connectors that can't enumerate all tables) - QualifiedObjectName name = createQualifiedObjectName(session, table, table.getName()); ImmutableList.Builder fields = ImmutableList.builder(); for (ColumnSchema column : tableSchema.getColumns()) { Field field = Field.newQualified( @@ -1626,14 +1642,14 @@ private List analyzeTableOutputFields(Table table, TableSchema tableSchem Optional.of(column.getName()), column.getType(), column.isHidden(), - Optional.of(name), + Optional.of(tableName), Optional.of(column.getName()), false); fields.add(field); ColumnHandle columnHandle = columnHandles.get(column.getName()); checkArgument(columnHandle != null, "Unknown field %s", field); analysis.setColumn(field, columnHandle); - analysis.addSourceColumns(field, ImmutableSet.of(new SourceColumn(name, column.getName()))); + analysis.addSourceColumns(field, ImmutableSet.of(new SourceColumn(tableName, column.getName()))); } return fields.build(); } 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 759aab2d7f39..81fd69203965 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 @@ -21,6 +21,7 @@ import io.trino.matching.Captures; import io.trino.matching.Pattern; import io.trino.metadata.Metadata; +import io.trino.metadata.QualifiedObjectName; import io.trino.metadata.TableHandle; import io.trino.metadata.TableMetadata; import io.trino.spi.TrinoException; @@ -48,6 +49,7 @@ import static io.trino.metadata.QualifiedObjectName.convertFromSchemaTableName; import static io.trino.spi.StandardErrorCode.COLUMN_NOT_FOUND; import static io.trino.spi.StandardErrorCode.FUNCTION_NOT_FOUND; +import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.StandardErrorCode.TABLE_NOT_FOUND; import static io.trino.sql.analyzer.TypeSignatureTranslator.toSqlType; import static io.trino.sql.planner.plan.Patterns.tableScan; @@ -86,6 +88,14 @@ public Result apply(TableScanNode scanNode, Captures captures, Context context) } CatalogSchemaTableName destinationTable = tableScanRedirectApplicationResult.get().getDestinationTable(); + + QualifiedObjectName destinationObjectName = convertFromSchemaTableName(destinationTable.getCatalogName()).apply(destinationTable.getSchemaTableName()); + Optional redirectedObjectName = metadata.getRedirectionAwareTableHandle(context.getSession(), destinationObjectName).getRedirectedTableName(); + + redirectedObjectName.ifPresent(name -> { + throw new TrinoException(NOT_SUPPORTED, format("Further redirection of destination table '%s' to '%s' is not supported", destinationObjectName, name)); + }); + TableMetadata tableMetadata = metadata.getTableMetadata(context.getSession(), scanNode.getTable()); CatalogSchemaTableName sourceTable = new CatalogSchemaTableName(tableMetadata.getCatalogName().getCatalogName(), tableMetadata.getTable()); if (destinationTable.equals(sourceTable)) { 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 8c756e0cbbb4..2c0ab5953bd2 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 @@ -28,6 +28,7 @@ import io.trino.metadata.Metadata; import io.trino.metadata.MetadataUtil; import io.trino.metadata.QualifiedObjectName; +import io.trino.metadata.RedirectionAwareTableHandle; import io.trino.metadata.SessionPropertyManager.SessionPropertyValue; import io.trino.metadata.TableHandle; import io.trino.security.AccessControl; @@ -231,6 +232,7 @@ protected Node visitShowTables(ShowTables showTables, Void context) @Override protected Node visitShowGrants(ShowGrants showGrants, Void context) { + // TODO: make this method redirection aware String catalogName = session.getCatalog().orElse(null); Optional predicate = Optional.empty(); @@ -389,10 +391,19 @@ protected Node visitShowColumns(ShowColumns showColumns, Void context) if (!metadata.schemaExists(session, new CatalogSchemaName(tableName.getCatalogName(), tableName.getSchemaName()))) { throw semanticException(SCHEMA_NOT_FOUND, showColumns, "Schema '%s' does not exist", tableName.getSchemaName()); } + Optional view = metadata.getView(session, tableName); - Optional tableHandle = metadata.getTableHandle(session, tableName); - if (view.isEmpty() && tableHandle.isEmpty()) { - throw semanticException(TABLE_NOT_FOUND, showColumns, "Table '%s' does not exist", tableName); + QualifiedObjectName targetTableName = tableName; + Optional tableHandle = Optional.empty(); + + // Check for table if view is not present + if (view.isEmpty()) { + RedirectionAwareTableHandle redirection = metadata.getRedirectionAwareTableHandle(session, tableName); + tableHandle = redirection.getTableHandle(); + if (tableHandle.isEmpty()) { + throw semanticException(TABLE_NOT_FOUND, showColumns, "Table '%s' does not exist", tableName); + } + targetTableName = redirection.getRedirectedTableName().orElse(tableName); } if (view.isEmpty() && tableHandle.isPresent()) { @@ -411,11 +422,12 @@ protected Node visitShowColumns(ShowColumns showColumns, Void context) // and we need to perform security filtering of the returned columns. metadata.getTableMetadata(session, tableHandle.get()); } - accessControl.checkCanShowColumns(session.toSecurityContext(), tableName.asCatalogSchemaTableName()); + + accessControl.checkCanShowColumns(session.toSecurityContext(), targetTableName.asCatalogSchemaTableName()); Expression predicate = logicalAnd( - equal(identifier("table_schema"), new StringLiteral(tableName.getSchemaName())), - equal(identifier("table_name"), new StringLiteral(tableName.getObjectName()))); + equal(identifier("table_schema"), new StringLiteral(targetTableName.getSchemaName())), + equal(identifier("table_name"), new StringLiteral(targetTableName.getObjectName()))); Optional likePattern = showColumns.getLikePattern(); if (likePattern.isPresent()) { Expression likePredicate = new LikePredicate( @@ -431,7 +443,7 @@ protected Node visitShowColumns(ShowColumns showColumns, Void context) aliasedName("data_type", "Type"), aliasedNullToEmpty("extra_info", "Extra"), aliasedNullToEmpty("comment", "Comment")), - from(tableName.getCatalogName(), COLUMNS.getSchemaTableName()), + from(targetTableName.getCatalogName(), COLUMNS.getSchemaTableName()), predicate, ordering(ascending("ordinal_position"))); } @@ -547,12 +559,14 @@ protected Node visitShowCreate(ShowCreate node, Void context) throw semanticException(NOT_SUPPORTED, node, "Relation '%s' is a view, not a table", objectName); } - Optional tableHandle = metadata.getTableHandle(session, objectName); + RedirectionAwareTableHandle redirection = metadata.getRedirectionAwareTableHandle(session, objectName); + Optional tableHandle = redirection.getTableHandle(); if (tableHandle.isEmpty()) { throw semanticException(TABLE_NOT_FOUND, node, "Table '%s' does not exist", objectName); } - accessControl.checkCanShowCreateTable(session.toSecurityContext(), objectName); + QualifiedObjectName targetTableName = redirection.getRedirectedTableName().orElse(objectName); + accessControl.checkCanShowCreateTable(session.toSecurityContext(), targetTableName); ConnectorTableMetadata connectorTableMetadata = metadata.getTableMetadata(session, tableHandle.get()).getMetadata(); Map> allColumnProperties = metadata.getColumnPropertyManager().getAllProperties().get(tableHandle.get().getCatalogName()); @@ -560,14 +574,14 @@ protected Node visitShowCreate(ShowCreate node, Void context) List columns = connectorTableMetadata.getColumns().stream() .filter(column -> !column.isHidden()) .map(column -> { - List propertyNodes = buildProperties(objectName, Optional.of(column.getName()), INVALID_COLUMN_PROPERTY, column.getProperties(), allColumnProperties); + List propertyNodes = buildProperties(targetTableName, Optional.of(column.getName()), INVALID_COLUMN_PROPERTY, column.getProperties(), allColumnProperties); return new ColumnDefinition(new Identifier(column.getName()), toSqlType(column.getType()), column.isNullable(), propertyNodes, Optional.ofNullable(column.getComment())); }) .collect(toImmutableList()); Map properties = connectorTableMetadata.getProperties(); Map> allTableProperties = metadata.getTablePropertyManager().getAllProperties().get(tableHandle.get().getCatalogName()); - List propertyNodes = buildProperties(objectName, Optional.empty(), INVALID_TABLE_PROPERTY, properties, allTableProperties); + List propertyNodes = buildProperties(targetTableName, Optional.empty(), INVALID_TABLE_PROPERTY, properties, allTableProperties); CreateTable createTable = new CreateTable( QualifiedName.of(objectName.getCatalogName(), objectName.getSchemaName(), objectName.getObjectName()), diff --git a/core/trino-main/src/test/java/io/trino/connector/MockConnector.java b/core/trino-main/src/test/java/io/trino/connector/MockConnector.java index 4ca9f2ae8cf7..353c56bfd3ea 100644 --- a/core/trino-main/src/test/java/io/trino/connector/MockConnector.java +++ b/core/trino-main/src/test/java/io/trino/connector/MockConnector.java @@ -18,6 +18,7 @@ import io.trino.spi.Page; import io.trino.spi.connector.AggregateFunction; import io.trino.spi.connector.AggregationApplicationResult; +import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.Connector; @@ -55,6 +56,7 @@ import io.trino.spi.connector.SchemaTableName; import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortItem; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.eventlistener.EventListener; @@ -75,6 +77,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableMap.toImmutableMap; @@ -88,6 +91,7 @@ public class MockConnector { private final Function> listSchemaNames; private final BiFunction> listTables; + private final Optional>> streamTableColumns; private final BiFunction> getViews; private final BiFunction> getMaterializedViews; private final BiFunction getTableHandle; @@ -98,6 +102,7 @@ public class MockConnector private final MockConnectorFactory.ApplyTopN applyTopN; private final MockConnectorFactory.ApplyFilter applyFilter; private final MockConnectorFactory.ApplyTableScanRedirect applyTableScanRedirect; + private final BiFunction> redirectTable; private final BiFunction> getInsertLayout; private final BiFunction> getNewTableLayout; private final BiFunction getTableProperties; @@ -108,6 +113,7 @@ public class MockConnector MockConnector( Function> listSchemaNames, BiFunction> listTables, + Optional>> streamTableColumns, BiFunction> getViews, BiFunction> getMaterializedViews, BiFunction getTableHandle, @@ -118,6 +124,7 @@ public class MockConnector MockConnectorFactory.ApplyTopN applyTopN, MockConnectorFactory.ApplyFilter applyFilter, MockConnectorFactory.ApplyTableScanRedirect applyTableScanRedirect, + BiFunction> redirectTable, BiFunction> getInsertLayout, BiFunction> getNewTableLayout, BiFunction getTableProperties, @@ -127,6 +134,7 @@ public class MockConnector { this.listSchemaNames = requireNonNull(listSchemaNames, "listSchemaNames is null"); this.listTables = requireNonNull(listTables, "listTables is null"); + this.streamTableColumns = requireNonNull(streamTableColumns, "streamTableColumns is null"); this.getViews = requireNonNull(getViews, "getViews is null"); this.getMaterializedViews = requireNonNull(getMaterializedViews, "getMaterializedViews is null"); this.getTableHandle = requireNonNull(getTableHandle, "getTableHandle is null"); @@ -137,6 +145,7 @@ public class MockConnector this.applyTopN = requireNonNull(applyTopN, "applyTopN is null"); this.applyFilter = requireNonNull(applyFilter, "applyFilter is null"); this.applyTableScanRedirect = requireNonNull(applyTableScanRedirect, "applyTableScanRedirection is null"); + this.redirectTable = requireNonNull(redirectTable, "redirectTable is null"); this.getInsertLayout = requireNonNull(getInsertLayout, "getInsertLayout is null"); this.getNewTableLayout = requireNonNull(getNewTableLayout, "getNewTableLayout is null"); this.getTableProperties = requireNonNull(getTableProperties, "getTableProperties is null"); @@ -261,6 +270,12 @@ public Optional applyTableScanRedirect(Conne return applyTableScanRedirect.apply(session, tableHandle); } + @Override + public Optional redirectTable(ConnectorSession session, SchemaTableName schemaTableName) + { + return redirectTable.apply(session, schemaTableName); + } + @Override public List listSchemaNames(ConnectorSession session) { @@ -318,11 +333,15 @@ public ColumnMetadata getColumnMetadata(ConnectorSession session, ConnectorTable } @Override - public Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + public Stream streamTableColumns(ConnectorSession session, SchemaTablePrefix prefix) { + if (streamTableColumns.isPresent()) { + return streamTableColumns.get().apply(session, prefix); + } + return listTables(session, prefix.getSchema()).stream() .filter(prefix::matches) - .collect(toImmutableMap(table -> table, getColumns)); + .map(name -> TableColumnsMetadata.forTable(name, getColumns.apply(name))); } @Override diff --git a/core/trino-main/src/test/java/io/trino/connector/MockConnectorFactory.java b/core/trino-main/src/test/java/io/trino/connector/MockConnectorFactory.java index be788f409e94..a074d60a2466 100644 --- a/core/trino-main/src/test/java/io/trino/connector/MockConnectorFactory.java +++ b/core/trino-main/src/test/java/io/trino/connector/MockConnectorFactory.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableSet; import io.trino.spi.connector.AggregateFunction; import io.trino.spi.connector.AggregationApplicationResult; +import io.trino.spi.connector.CatalogSchemaTableName; import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.Connector; @@ -40,6 +41,7 @@ import io.trino.spi.connector.SchemaTableName; import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortItem; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.eventlistener.EventListener; @@ -56,6 +58,7 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.IntStream; +import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.toImmutableList; import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; @@ -66,6 +69,7 @@ public class MockConnectorFactory { private final Function> listSchemaNames; private final BiFunction> listTables; + Optional>> streamTableColumns; private final BiFunction> getViews; private final BiFunction> getMaterializedViews; private final BiFunction getTableHandle; @@ -76,6 +80,7 @@ public class MockConnectorFactory private final ApplyTopN applyTopN; private final ApplyFilter applyFilter; private final ApplyTableScanRedirect applyTableScanRedirect; + private final BiFunction> redirectTable; private final BiFunction> getInsertLayout; private final BiFunction> getNewTableLayout; private final BiFunction getTableProperties; @@ -86,6 +91,7 @@ public class MockConnectorFactory private MockConnectorFactory( Function> listSchemaNames, BiFunction> listTables, + Optional>> streamTableColumns, BiFunction> getViews, BiFunction> getMaterializedViews, BiFunction getTableHandle, @@ -96,6 +102,7 @@ private MockConnectorFactory( ApplyTopN applyTopN, ApplyFilter applyFilter, ApplyTableScanRedirect applyTableScanRedirect, + BiFunction> redirectTable, BiFunction> getInsertLayout, BiFunction> getNewTableLayout, BiFunction getTableProperties, @@ -105,6 +112,7 @@ private MockConnectorFactory( { this.listSchemaNames = requireNonNull(listSchemaNames, "listSchemaNames is null"); this.listTables = requireNonNull(listTables, "listTables is null"); + this.streamTableColumns = requireNonNull(streamTableColumns, "streamTableColumns is null"); this.getViews = requireNonNull(getViews, "getViews is null"); this.getMaterializedViews = requireNonNull(getMaterializedViews, "getMaterializedViews is null"); this.getTableHandle = requireNonNull(getTableHandle, "getTableHandle is null"); @@ -115,6 +123,7 @@ private MockConnectorFactory( this.applyTopN = requireNonNull(applyTopN, "applyTopN is null"); this.applyFilter = requireNonNull(applyFilter, "applyFilter is null"); this.applyTableScanRedirect = requireNonNull(applyTableScanRedirect, "applyTableScanRedirection is null"); + this.redirectTable = requireNonNull(redirectTable, "redirectTable is null"); this.getInsertLayout = requireNonNull(getInsertLayout, "getInsertLayout is null"); this.getNewTableLayout = requireNonNull(getNewTableLayout, "getNewTableLayout is null"); this.getTableProperties = requireNonNull(getTableProperties, "getTableProperties is null"); @@ -141,6 +150,7 @@ public Connector create(String catalogName, Map config, Connecto return new MockConnector( listSchemaNames, listTables, + streamTableColumns, getViews, getMaterializedViews, getTableHandle, @@ -151,6 +161,7 @@ public Connector create(String catalogName, Map config, Connecto applyTopN, applyFilter, applyTableScanRedirect, + redirectTable, getInsertLayout, getNewTableLayout, getTableProperties, @@ -231,6 +242,7 @@ public static final class Builder { private Function> listSchemaNames = defaultListSchemaNames(); private BiFunction> listTables = defaultListTables(); + private Optional>> streamTableColumns = Optional.empty(); private BiFunction> getViews = defaultGetViews(); private BiFunction> getMaterializedViews = defaultGetMaterializedViews(); private BiFunction getTableHandle = defaultGetTableHandle(); @@ -250,6 +262,7 @@ public static final class Builder private ApplyTableScanRedirect applyTableScanRedirect = (session, handle) -> Optional.empty(); private Function rowFilter = (tableName) -> null; private BiFunction columnMask = (tableName, columnName) -> null; + private BiFunction> redirectTable = (session, tableName) -> Optional.empty(); public Builder withListSchemaNames(Function> listSchemaNames) { @@ -269,6 +282,12 @@ public Builder withListTables(BiFunction> streamTableColumns) + { + this.streamTableColumns = Optional.of(requireNonNull(streamTableColumns, "streamTableColumns is null")); + return this; + } + public Builder withGetViews(BiFunction> getViews) { this.getViews = requireNonNull(getViews, "getViews is null"); @@ -329,6 +348,12 @@ public Builder withApplyTableScanRedirect(ApplyTableScanRedirect applyTableScanR return this; } + public Builder withRedirectTable(BiFunction> redirectTable) + { + this.redirectTable = requireNonNull(redirectTable, "redirectTable is null"); + return this; + } + public Builder withGetInsertLayout(BiFunction> getInsertLayout) { this.getInsertLayout = requireNonNull(getInsertLayout, "getInsertLayout is null"); @@ -392,6 +417,7 @@ public MockConnectorFactory build() return new MockConnectorFactory( listSchemaNames, listTables, + streamTableColumns, getViews, getMaterializedViews, getTableHandle, @@ -402,6 +428,7 @@ public MockConnectorFactory build() applyTopN, applyFilter, applyTableScanRedirect, + redirectTable, getInsertLayout, getNewTableLayout, getTableProperties, 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 94a17d607a01..9b05ba4fb125 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 @@ -48,6 +48,7 @@ import io.trino.spi.connector.SampleType; import io.trino.spi.connector.SortItem; import io.trino.spi.connector.SystemTable; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.expression.ConnectorExpression; @@ -78,6 +79,7 @@ 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; @@ -200,7 +202,7 @@ public ColumnMetadata getColumnMetadata(Session session, TableHandle tableHandle } @Override - public Map> listTableColumns(Session session, QualifiedTablePrefix prefix) + public Map> listTableColumns(Session session, QualifiedTablePrefix prefix) { throw new UnsupportedOperationException(); } @@ -866,4 +868,10 @@ public Optional applyTableScanRedirect(Sessi { throw new UnsupportedOperationException(); } + + @Override + public RedirectionAwareTableHandle getRedirectionAwareTableHandle(Session session, QualifiedObjectName tableName) + { + return noRedirection(getTableHandle(session, tableName)); + } } diff --git a/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java b/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java index c83329cff700..ae6df3434484 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java +++ b/core/trino-spi/src/main/java/io/trino/spi/StandardErrorCode.java @@ -124,6 +124,7 @@ public enum StandardErrorCode TABLE_HAS_NO_COLUMNS(101, USER_ERROR), INVALID_RANGE(102, USER_ERROR), INVALID_PATTERN_RECOGNITION_FUNCTION(103, USER_ERROR), + TABLE_REDIRECTION_ERROR(104, USER_ERROR), GENERIC_INTERNAL_ERROR(65536, INTERNAL_ERROR), TOO_MANY_REQUESTS_FAILED(65537, INTERNAL_ERROR), diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java index f6bcd3b0c43c..8e7949d04106 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/ConnectorMetadata.java @@ -37,6 +37,7 @@ import java.util.OptionalLong; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.trino.spi.StandardErrorCode.GENERIC_INTERNAL_ERROR; import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; @@ -228,12 +229,24 @@ default ColumnMetadata getColumnMetadata(ConnectorSession session, ConnectorTabl /** * Gets the metadata for all columns that match the specified table prefix. + * @deprecated use {@link #streamTableColumns} which handles redirected tables */ + @Deprecated default Map> listTableColumns(ConnectorSession session, SchemaTablePrefix prefix) { return emptyMap(); } + /** + * Gets the metadata for all columns that match the specified table prefix. Redirected table names are included, but + * the column metadata for them is not. + */ + default Stream streamTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + return listTableColumns(session, prefix).entrySet().stream() + .map(entry -> TableColumnsMetadata.forTable(entry.getKey(), entry.getValue())); + } + /** * Get statistics for table for given filtering constraint. */ @@ -1145,4 +1158,15 @@ default Optional applyTableScanRedirect(Conn { return Optional.empty(); } + + /** + * Redirects table to other table which may or may not be in the same catalog. + * Currently the engine tries to do redirection only for table reads and metadata listing. + * + * Also consider implementing streamTableColumns to support redirection for listing. + */ + default Optional redirectTable(ConnectorSession session, SchemaTableName tableName) + { + return Optional.empty(); + } } diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/SchemaTableName.java b/core/trino-spi/src/main/java/io/trino/spi/connector/SchemaTableName.java index a2c27763b344..29b09cde6b61 100644 --- a/core/trino-spi/src/main/java/io/trino/spi/connector/SchemaTableName.java +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/SchemaTableName.java @@ -33,6 +33,11 @@ public SchemaTableName(@JsonProperty("schema") String schemaName, @JsonProperty( this.tableName = checkNotEmpty(tableName, "tableName").toLowerCase(ENGLISH); } + public static SchemaTableName schemaTableName(String schemaName, String tableName) + { + return new SchemaTableName(schemaName, tableName); + } + @JsonProperty("schema") public String getSchemaName() { diff --git a/core/trino-spi/src/main/java/io/trino/spi/connector/TableColumnsMetadata.java b/core/trino-spi/src/main/java/io/trino/spi/connector/TableColumnsMetadata.java new file mode 100644 index 000000000000..fe5890e8e4bd --- /dev/null +++ b/core/trino-spi/src/main/java/io/trino/spi/connector/TableColumnsMetadata.java @@ -0,0 +1,54 @@ +/* + * 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.spi.connector; + +import java.util.List; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class TableColumnsMetadata +{ + private final SchemaTableName table; + private final Optional> columns; + + public TableColumnsMetadata(SchemaTableName table, Optional> columns) + { + this.table = requireNonNull(table, "table is null"); + this.columns = requireNonNull(columns, "columns is null"); + } + + public static TableColumnsMetadata forTable(SchemaTableName table, List columns) + { + return new TableColumnsMetadata(table, Optional.of(requireNonNull(columns, "columns is null"))); + } + + public static TableColumnsMetadata forRedirectedTable(SchemaTableName table) + { + return new TableColumnsMetadata(table, Optional.empty()); + } + + public SchemaTableName getTable() + { + return table; + } + + /** + * @return Optional.empty() value means the table is redirected + */ + public Optional> getColumns() + { + return columns; + } +} diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java index 1bb31955d5dc..7f628e1b89fa 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/classloader/ClassLoaderSafeConnectorMetadata.java @@ -18,6 +18,7 @@ import io.trino.spi.connector.AggregateFunction; import io.trino.spi.connector.AggregationApplicationResult; 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; @@ -51,6 +52,7 @@ import io.trino.spi.connector.SchemaTablePrefix; import io.trino.spi.connector.SortItem; import io.trino.spi.connector.SystemTable; +import io.trino.spi.connector.TableColumnsMetadata; import io.trino.spi.connector.TableScanRedirectApplicationResult; import io.trino.spi.connector.TopNApplicationResult; import io.trino.spi.expression.ConnectorExpression; @@ -71,6 +73,7 @@ import java.util.Optional; import java.util.OptionalLong; import java.util.Set; +import java.util.stream.Stream; import static java.util.Objects.requireNonNull; @@ -283,6 +286,14 @@ public Map> listTableColumns(ConnectorSess } } + @Override + public Stream streamTableColumns(ConnectorSession session, SchemaTablePrefix prefix) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + return delegate.streamTableColumns(session, prefix); + } + } + @Override public TableStatistics getTableStatistics(ConnectorSession session, ConnectorTableHandle tableHandle, Constraint constraint) { @@ -890,4 +901,12 @@ public void finishUpdate(ConnectorSession session, ConnectorTableHandle tableHan delegate.finishUpdate(session, tableHandle, fragments); } } + + @Override + public Optional redirectTable(ConnectorSession session, SchemaTableName tableName) + { + try (ThreadContextClassLoader ignored = new ThreadContextClassLoader(classLoader)) { + return delegate.redirectTable(session, tableName); + } + } } diff --git a/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestQueryFramework.java b/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestQueryFramework.java index e0cb96b33638..08c7d9310501 100644 --- a/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestQueryFramework.java +++ b/testing/trino-testing/src/main/java/io/trino/testing/AbstractTestQueryFramework.java @@ -171,6 +171,11 @@ protected void assertQuery(Session session, @Language("SQL") String actual, @Lan QueryAssertions.assertQuery(queryRunner, session, actual, h2QueryRunner, expected, false, false); } + protected void assertQuery(@Language("SQL") String actual, @Language("SQL") String expected, Consumer planAssertion) + { + assertQuery(getSession(), actual, expected, planAssertion); + } + protected void assertQuery(Session session, @Language("SQL") String actual, @Language("SQL") String expected, Consumer planAssertion) { checkArgument(queryRunner instanceof DistributedQueryRunner, "pattern assertion is only supported for DistributedQueryRunner"); 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 new file mode 100644 index 000000000000..32231a3e4f56 --- /dev/null +++ b/testing/trino-tests/src/test/java/io/trino/execution/TestTableRedirection.java @@ -0,0 +1,385 @@ +/* + * 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.execution; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.trino.Session; +import io.trino.connector.MockConnectorFactory; +import io.trino.connector.MockConnectorPlugin; +import io.trino.connector.MockConnectorTableHandle; +import io.trino.spi.connector.CatalogSchemaTableName; +import io.trino.spi.connector.ColumnMetadata; +import io.trino.spi.connector.SchemaTableName; +import io.trino.spi.connector.TableColumnsMetadata; +import io.trino.sql.planner.Plan; +import io.trino.sql.planner.plan.TableScanNode; +import io.trino.testing.AbstractTestQueryFramework; +import io.trino.testing.DistributedQueryRunner; +import io.trino.testing.QueryRunner; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static io.trino.metadata.MetadataManager.MAX_TABLE_REDIRECTIONS; +import static io.trino.spi.connector.SchemaTableName.schemaTableName; +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.sql.planner.optimizations.PlanNodeSearcher.searchFrom; +import static io.trino.testing.TestingSession.testSessionBuilder; +import static io.trino.testing.assertions.Assert.assertEquals; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestTableRedirection + extends AbstractTestQueryFramework +{ + private static final String CATALOG_NAME = "test_catalog"; + private static final String SCHEMA_ONE = "test_schema_1"; + private static final String SCHEMA_TWO = "test_schema_2"; + private static final String SCHEMA_THREE = "test_schema_3"; + private static final List SCHEMAS = ImmutableList.of(SCHEMA_ONE, SCHEMA_TWO, SCHEMA_THREE); + private static final String TABLE_FOO = "table_foo"; + private static final String TABLE_BAR = "table_bar"; + private static final String VALID_REDIRECTION_SRC = "valid_redirection_src"; + private static final String VALID_REDIRECTION_TARGET = "valid_redirection_target"; + private static final String BAD_REDIRECTION_SRC = "bad_redirection_src"; + private static final String NON_EXISTENT_TABLE = "non_existent_table"; + private static final String REDIRECTION_TWICE_SRC = "redirection_twice_src"; + private static final String INTERMEDIATE_TABLE = "intermediate_table"; + private static final String REDIRECTION_LOOP_PING = "redirection_loop_ping"; + private static final String REDIRECTION_LOOP_PONG = "redirection_loop_pong"; + private static final List REDIRECTION_CHAIN = IntStream.range(0, MAX_TABLE_REDIRECTIONS + 1).boxed() + .map(i -> "redirection_chain_table_" + i) + .collect(toImmutableList()); + private static final String C0 = "c0"; + private static final String C1 = "c1"; + private static final String C2 = "c2"; + private static final String C3 = "c3"; + private static final String C4 = "c4"; + + private static final Map> SCHEMA_TABLE_MAPPING = ImmutableMap.of( + SCHEMA_ONE, + ImmutableSet.of(TABLE_FOO, VALID_REDIRECTION_SRC, BAD_REDIRECTION_SRC, REDIRECTION_TWICE_SRC, REDIRECTION_LOOP_PING), + SCHEMA_TWO, + ImmutableSet.of(TABLE_BAR, VALID_REDIRECTION_TARGET, INTERMEDIATE_TABLE, REDIRECTION_LOOP_PONG), + SCHEMA_THREE, + ImmutableSet.copyOf(REDIRECTION_CHAIN)); + + private static final Map REDIRECTIONS = ImmutableMap.builder() + // Redirection to a valid table + .put(schemaTableName(SCHEMA_ONE, VALID_REDIRECTION_SRC), schemaTableName(SCHEMA_TWO, VALID_REDIRECTION_TARGET)) + // Redirection to a non existent table + .put(schemaTableName(SCHEMA_ONE, BAD_REDIRECTION_SRC), schemaTableName(SCHEMA_TWO, NON_EXISTENT_TABLE)) + // Multi step redirection + .put(schemaTableName(SCHEMA_ONE, REDIRECTION_TWICE_SRC), schemaTableName(SCHEMA_TWO, INTERMEDIATE_TABLE)) + .put(schemaTableName(SCHEMA_TWO, INTERMEDIATE_TABLE), schemaTableName(SCHEMA_ONE, TABLE_FOO)) + // Redirection loop + .put(schemaTableName(SCHEMA_ONE, REDIRECTION_LOOP_PING), schemaTableName(SCHEMA_TWO, REDIRECTION_LOOP_PONG)) + .put(schemaTableName(SCHEMA_TWO, REDIRECTION_LOOP_PONG), schemaTableName(SCHEMA_ONE, REDIRECTION_LOOP_PING)) + // Redirection chain: redirection_chain_table_0 -> redirection_chain_table_1 -> redirection_chain_table_2 ... + .putAll(IntStream.range(0, REDIRECTION_CHAIN.size() - 1) + .boxed() + .collect(toImmutableMap( + i -> schemaTableName(SCHEMA_THREE, REDIRECTION_CHAIN.get(i)), + i -> schemaTableName(SCHEMA_THREE, REDIRECTION_CHAIN.get(i + 1))))) + .build(); + + private static final Map> columnMetadatas = ImmutableMap.of( + SCHEMA_ONE, + ImmutableList.of( + new ColumnMetadata(C0, BIGINT), + new ColumnMetadata(C1, BIGINT)), + SCHEMA_TWO, + ImmutableList.of( + new ColumnMetadata(C2, BIGINT), + new ColumnMetadata(C3, BIGINT)), + SCHEMA_THREE, + ImmutableList.of(new ColumnMetadata(C4, BIGINT))); + + private static final Function> columnsGetter = table -> { + List columns = columnMetadatas.get(table.getSchemaName()); + if (columns != null) { + return columns; + } + throw new RuntimeException(format("Unknown schema: %s", table.getSchemaName())); + }; + + private static final Session TEST_SESSION = testSessionBuilder() + .setCatalog(CATALOG_NAME) + .build(); + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + QueryRunner queryRunner = DistributedQueryRunner.builder(TEST_SESSION).build(); + queryRunner.installPlugin(new MockConnectorPlugin(createMockConnectorFactory())); + queryRunner.createCatalog(CATALOG_NAME, "mock", ImmutableMap.of()); + return queryRunner; + } + + private MockConnectorFactory createMockConnectorFactory() + { + return MockConnectorFactory.builder() + .withListSchemaNames(session -> SCHEMAS) + .withListTables((session, schemaName) -> SCHEMA_TABLE_MAPPING.getOrDefault(schemaName, ImmutableSet.of()).stream() + .map(name -> new SchemaTableName(schemaName, name)) + .collect(toImmutableList())) + .withStreamTableColumns((session, prefix) -> { + List allColumnsMetadata = SCHEMA_TABLE_MAPPING.entrySet().stream() + .flatMap(entry -> entry.getValue().stream().map(table -> new SchemaTableName(entry.getKey(), table))) + .map(schemaTableName -> { + if (REDIRECTIONS.containsKey(schemaTableName)) { + return TableColumnsMetadata.forRedirectedTable(schemaTableName); + } + return TableColumnsMetadata.forTable(schemaTableName, columnsGetter.apply(schemaTableName)); + }) + .collect(toImmutableList()); + + if (prefix.isEmpty()) { + return allColumnsMetadata.stream(); + } + + String schema = prefix.getSchema().get(); + + if (SCHEMAS.contains(schema)) { + return allColumnsMetadata.stream() + .filter(columnsMetadata -> columnsMetadata.getTable().getSchemaName().equals(schema)) + .filter(columnsMetadata -> prefix.getTable().map(columnsMetadata.getTable().getTableName()::equals).orElse(true)); + } + + return Stream.empty(); + }) + .withGetTableHandle((session, tableName) -> { + if (SCHEMA_TABLE_MAPPING.getOrDefault(tableName.getSchemaName(), ImmutableSet.of()).contains(tableName.getTableName()) + && !REDIRECTIONS.containsKey(tableName)) { + return new MockConnectorTableHandle(tableName); + } + return null; + }) + .withGetViews(((connectorSession, prefix) -> ImmutableMap.of())) + .withGetColumns(schemaTableName -> { + if (!REDIRECTIONS.containsKey(schemaTableName)) { + return columnsGetter.apply(schemaTableName); + } + + throw new RuntimeException("Columns do not exist for: " + schemaTableName); + }) + .withRedirectTable(((connectorSession, schemaTableName) -> { + return Optional.ofNullable(REDIRECTIONS.get(schemaTableName)) + .map(target -> new CatalogSchemaTableName(CATALOG_NAME, target)); + })) + .build(); + } + + @Test + public void testTableScans() + { + assertQuery( + format("SELECT c2 FROM %s.%s", SCHEMA_ONE, VALID_REDIRECTION_SRC), + "SELECT 1 WHERE 1=0", + verifySingleTableScan(SCHEMA_TWO, VALID_REDIRECTION_TARGET)); + + assertThatThrownBy(() -> query((format("SELECT c0 FROM %s.%s", SCHEMA_ONE, BAD_REDIRECTION_SRC)))) + .hasMessageContaining( + "Table '%s' redirected to '%s', but the target table '%s' does not exist", + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, BAD_REDIRECTION_SRC), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE)); + + assertQuery( + format("SELECT c0 FROM %s.%s", SCHEMA_ONE, REDIRECTION_TWICE_SRC), + "SELECT 1 WHERE 1=0", + verifySingleTableScan(SCHEMA_ONE, TABLE_FOO)); + + assertThatThrownBy(() -> query(format("SELECT c0 FROM %s.%s", SCHEMA_ONE, REDIRECTION_LOOP_PING))) + .hasMessageContaining(format( + "Table redirections form a loop: %s -> %s -> %s", + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, REDIRECTION_LOOP_PING), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, REDIRECTION_LOOP_PONG), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, REDIRECTION_LOOP_PING))); + + assertThatThrownBy(() -> query(format("SELECT c4 FROM %s.%s", SCHEMA_THREE, REDIRECTION_CHAIN.get(0)))) + .hasMessageContaining(format( + "Table redirected too many times (10): [%s]", + REDIRECTION_CHAIN.stream() + .map(table -> new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_THREE, table).toString()) + .collect(Collectors.joining(", ")))); + } + + @Test + public void testTableListing() + { + assertQuery( + format("SHOW TABLES FROM %s", SCHEMA_ONE), + format("VALUES %s", + SCHEMA_TABLE_MAPPING.get(SCHEMA_ONE).stream() + .map(table -> "('" + table + "')") + .collect(Collectors.joining(",")))); + assertQuery( + format("SELECT table_name FROM system.jdbc.tables WHERE table_cat = '%s' AND table_schem ='%s'", CATALOG_NAME, SCHEMA_ONE), + format("VALUES %s", + SCHEMA_TABLE_MAPPING.get(SCHEMA_ONE).stream() + .map(table -> "('" + table + "')") + .collect(Collectors.joining(",")))); + + assertQuery( + format("SHOW TABLES FROM %s", SCHEMA_TWO), + format( + "VALUES %s", + SCHEMA_TABLE_MAPPING.get(SCHEMA_TWO).stream() + .map(table -> "('" + table + "')") + .collect(Collectors.joining(",")))); + assertQuery( + format("SELECT table_name FROM system.jdbc.tables WHERE table_cat = '%s' AND table_schem ='%s'", CATALOG_NAME, SCHEMA_TWO), + format("VALUES %s", + SCHEMA_TABLE_MAPPING.get(SCHEMA_TWO).stream() + .map(table -> "('" + table + "')") + .collect(Collectors.joining(",")))); + + assertQuery( + "SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema != 'information_schema'", + format( + "VALUES %s", + SCHEMA_TABLE_MAPPING.entrySet().stream() + .map(mappings -> mappings.getValue().stream() + .map(tableName -> row(mappings.getKey(), tableName))) + .flatMap(Function.identity()) + .collect(Collectors.joining(",")))); + } + + @Test + public void testTableColumnsListing() + { + String schemaOneColumns = "VALUES " + + row(SCHEMA_ONE, TABLE_FOO, C0) + "," + + row(SCHEMA_ONE, TABLE_FOO, C1) + "," + + row(SCHEMA_ONE, VALID_REDIRECTION_SRC, C2) + "," + + row(SCHEMA_ONE, VALID_REDIRECTION_SRC, C3) + "," + + row(SCHEMA_ONE, REDIRECTION_TWICE_SRC, C0) + "," + + row(SCHEMA_ONE, REDIRECTION_TWICE_SRC, C1); + assertQuery(format("SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = '%s'", SCHEMA_ONE), schemaOneColumns); + assertQuery(format("SELECT table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_schem = '%s' AND table_cat = '%s'", SCHEMA_ONE, CATALOG_NAME), schemaOneColumns); + + String schemaTwoColumns = "VALUES " + + row(SCHEMA_TWO, TABLE_BAR, C2) + "," + + row(SCHEMA_TWO, TABLE_BAR, C3) + "," + + row(SCHEMA_TWO, VALID_REDIRECTION_TARGET, C2) + "," + + row(SCHEMA_TWO, VALID_REDIRECTION_TARGET, C3) + "," + + row(SCHEMA_TWO, INTERMEDIATE_TABLE, C0) + "," + + row(SCHEMA_TWO, INTERMEDIATE_TABLE, C1); + assertQuery(format("SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = '%s'", SCHEMA_TWO), schemaTwoColumns); + assertQuery(format("SELECT table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_schem = '%s' AND table_cat = '%s'", SCHEMA_TWO, CATALOG_NAME), schemaTwoColumns); + + String validRedirectionSrcColumns = "VALUES " + + row(SCHEMA_ONE, VALID_REDIRECTION_SRC, C2) + "," + + row(SCHEMA_ONE, VALID_REDIRECTION_SRC, C3); + assertQuery(format("SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = '%s' AND table_name = '%s'", SCHEMA_ONE, VALID_REDIRECTION_SRC), validRedirectionSrcColumns); + assertQuery(format("SELECT table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_schem = '%s' AND table_name='%s' AND table_cat = '%s'", SCHEMA_ONE, VALID_REDIRECTION_SRC, CATALOG_NAME), validRedirectionSrcColumns); + + String emptyResult = "SELECT '', '', '' WHERE 1 = 0"; + assertQuery(format("SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = '%s' AND table_name = '%s'", SCHEMA_ONE, BAD_REDIRECTION_SRC), emptyResult); + assertQuery(format("SELECT table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_schem = '%s' AND table_name='%s' AND table_cat = '%s'", SCHEMA_ONE, BAD_REDIRECTION_SRC, CATALOG_NAME), emptyResult); + + assertQuery(format("SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE table_schema = '%s' AND table_name = '%s'", SCHEMA_ONE, REDIRECTION_LOOP_PING), emptyResult); + assertQuery(format("SELECT table_schem, table_name, column_name FROM system.jdbc.columns WHERE table_schem = '%s' AND table_name = '%s' AND table_cat = '%s'", SCHEMA_ONE, REDIRECTION_LOOP_PING, CATALOG_NAME), emptyResult); + } + + @Test + public void testShowCreate() + { + String showCreateValidSource = (String) computeScalar(format("SHOW CREATE TABLE %s.%s", SCHEMA_ONE, VALID_REDIRECTION_SRC)); + String showCreateValidTarget = (String) computeScalar(format("SHOW CREATE TABLE %s.%s", SCHEMA_TWO, VALID_REDIRECTION_TARGET)); + assertEquals(showCreateValidTarget, showCreateValidSource.replace(SCHEMA_ONE + "." + VALID_REDIRECTION_SRC, SCHEMA_TWO + "." + VALID_REDIRECTION_TARGET)); + + assertThatThrownBy(() -> query((format("SHOW CREATE TABLE %s.%s", SCHEMA_ONE, BAD_REDIRECTION_SRC)))) + .hasMessageContaining(format( + "Table '%s' redirected to '%s', but the target table '%s' does not exist", + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, BAD_REDIRECTION_SRC), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE))); + + assertThatThrownBy(() -> query((format("SHOW CREATE TABLE %s.%s", SCHEMA_ONE, REDIRECTION_LOOP_PING)))) + .hasMessageContaining("Table redirections form a loop"); + } + + @Test + public void testDescribeTable() + { + assertEquals(computeActual(format("DESCRIBE %s.%s", SCHEMA_ONE, VALID_REDIRECTION_SRC)), + computeActual(format("DESCRIBE %s.%s", SCHEMA_TWO, VALID_REDIRECTION_TARGET))); + + assertThatThrownBy(() -> query((format("DESCRIBE %s.%s", SCHEMA_ONE, BAD_REDIRECTION_SRC)))) + .hasMessageContaining(format( + "Table '%s' redirected to '%s', but the target table '%s' does not exist", + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, BAD_REDIRECTION_SRC), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE))); + + assertThatThrownBy(() -> query((format("DESCRIBE %s.%s", SCHEMA_ONE, REDIRECTION_LOOP_PING)))) + .hasMessageContaining("Table redirections form a loop"); + } + + @Test + public void testShowColumns() + { + assertQuery( + format("SHOW COLUMNS FROM %s.%s", SCHEMA_ONE, VALID_REDIRECTION_SRC), + "VALUES " + + row(C2, BIGINT.getDisplayName(), "", "") + "," + + row(C3, BIGINT.getDisplayName(), "", "")); + + assertThatThrownBy(() -> query((format("SHOW COLUMNS FROM %s.%s", SCHEMA_ONE, BAD_REDIRECTION_SRC)))) + .hasMessageContaining(format( + "Table '%s' redirected to '%s', but the target table '%s' does not exist", + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_ONE, BAD_REDIRECTION_SRC), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE), + new CatalogSchemaTableName(CATALOG_NAME, SCHEMA_TWO, NON_EXISTENT_TABLE))); + + assertThatThrownBy(() -> query((format("SHOW COLUMNS FROM %s.%s", SCHEMA_ONE, REDIRECTION_LOOP_PING)))) + .hasMessageContaining("Table redirections form a loop"); + } + + // TODO: Add tests for redirection in CommentsSystemTable and CREATE TABLE LIKE + + private static String row(String... values) + { + return Arrays.stream(values) + .map(value -> "'" + value + "'") + .collect(Collectors.joining(",", "(", ")")); + } + + private Consumer verifySingleTableScan(String schemaName, String tableName) + { + return plan -> { + TableScanNode tableScan = searchFrom(plan.getRoot()) + .where(TableScanNode.class::isInstance) + .findOnlyElement(); + SchemaTableName actual = ((MockConnectorTableHandle) tableScan.getTable().getConnectorHandle()).getTableName(); + assertEquals(actual, schemaTableName(schemaName, tableName)); + }; + } +}