/*
* 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 com.facebook.presto.connector.informationSchema;
import com.facebook.presto.Session;
import com.facebook.presto.metadata.InternalTable;
import com.facebook.presto.metadata.Metadata;
import com.facebook.presto.metadata.OperatorNotFoundException;
import com.facebook.presto.metadata.QualifiedObjectName;
import com.facebook.presto.metadata.QualifiedTablePrefix;
import com.facebook.presto.metadata.Signature;
import com.facebook.presto.metadata.TableHandle;
import com.facebook.presto.metadata.TableLayout;
import com.facebook.presto.metadata.TableLayoutResult;
import com.facebook.presto.metadata.ViewDefinition;
import com.facebook.presto.security.AccessControl;
import com.facebook.presto.spi.ColumnHandle;
import com.facebook.presto.spi.ColumnMetadata;
import com.facebook.presto.spi.ConnectorPageSource;
import com.facebook.presto.spi.ConnectorSession;
import com.facebook.presto.spi.ConnectorSplit;
import com.facebook.presto.spi.Constraint;
import com.facebook.presto.spi.FixedPageSource;
import com.facebook.presto.spi.Page;
import com.facebook.presto.spi.QueryId;
import com.facebook.presto.spi.SchemaTableName;
import com.facebook.presto.spi.TableNotFoundException;
import com.facebook.presto.spi.block.Block;
import com.facebook.presto.spi.connector.ConnectorPageSourceProvider;
import com.facebook.presto.spi.connector.ConnectorTransactionHandle;
import com.facebook.presto.spi.predicate.NullableValue;
import com.facebook.presto.spi.predicate.TupleDomain;
import com.facebook.presto.spi.security.GrantInfo;
import com.facebook.presto.spi.security.PrivilegeInfo;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import io.airlift.slice.Slice;
import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_COLUMNS;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_INTERNAL_PARTITIONS;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_SCHEMATA;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_TABLES;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_TABLE_PRIVILEGES;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.TABLE_VIEWS;
import static com.facebook.presto.connector.informationSchema.InformationSchemaMetadata.informationSchemaTableColumns;
import static com.facebook.presto.metadata.MetadataListing.listSchemas;
import static com.facebook.presto.metadata.MetadataListing.listTableColumns;
import static com.facebook.presto.metadata.MetadataListing.listTablePrivileges;
import static com.facebook.presto.metadata.MetadataListing.listTables;
import static com.facebook.presto.metadata.MetadataListing.listViews;
import static com.facebook.presto.spi.type.VarcharType.createUnboundedVarcharType;
import static com.facebook.presto.spi.type.Varchars.isVarcharType;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Sets.union;
import static java.lang.String.format;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
public class InformationSchemaPageSourceProvider
implements ConnectorPageSourceProvider
{
private final Metadata metadata;
private final AccessControl accessControl;
public InformationSchemaPageSourceProvider(Metadata metadata, AccessControl accessControl)
{
this.metadata = requireNonNull(metadata, "metadata is null");
this.accessControl = requireNonNull(accessControl, "accessControl is null");
}
@Override
public ConnectorPageSource createPageSource(ConnectorTransactionHandle transactionHandle, ConnectorSession session, ConnectorSplit split, List<ColumnHandle> columns)
{
InternalTable table = getInternalTable(transactionHandle, session, split, columns);
List<Integer> channels = new ArrayList<>();
for (ColumnHandle column : columns) {
String columnName = ((InformationSchemaColumnHandle) column).getColumnName();
int columnIndex = table.getColumnIndex(columnName);
channels.add(columnIndex);
}
ImmutableList.Builder<Page> pages = ImmutableList.builder();
for (Page page : table.getPages()) {
Block[] blocks = new Block[channels.size()];
for (int index = 0; index < blocks.length; index++) {
blocks[index] = page.getBlock(channels.get(index));
}
pages.add(new Page(page.getPositionCount(), blocks));
}
return new FixedPageSource(pages.build());
}
private InternalTable getInternalTable(ConnectorTransactionHandle transactionHandle, ConnectorSession connectorSession, ConnectorSplit connectorSplit, List<ColumnHandle> columns)
{
InformationSchemaTransactionHandle transaction = (InformationSchemaTransactionHandle) transactionHandle;
InformationSchemaSplit split = (InformationSchemaSplit) connectorSplit;
requireNonNull(columns, "columns is null");
InformationSchemaTableHandle handle = split.getTableHandle();
Map<String, NullableValue> filters = split.getFilters();
Session session = Session.builder(metadata.getSessionPropertyManager())
.setTransactionId(transaction.getTransactionId())
.setQueryId(new QueryId(connectorSession.getQueryId()))
.setIdentity(connectorSession.getIdentity())
.setSource("information_schema")
.setCatalog("") // default catalog is not be used
.setSchema("") // default schema is not be used
.setTimeZoneKey(connectorSession.getTimeZoneKey())
.setLocale(connectorSession.getLocale())
.setStartTime(connectorSession.getStartTime())
.build();
return getInformationSchemaTable(session, handle.getCatalogName(), handle.getSchemaTableName(), filters);
}
public InternalTable getInformationSchemaTable(Session session, String catalog, SchemaTableName table, Map<String, NullableValue> filters)
{
if (table.equals(TABLE_COLUMNS)) {
return buildColumns(session, catalog, filters);
}
if (table.equals(TABLE_TABLES)) {
return buildTables(session, catalog, filters);
}
if (table.equals(TABLE_VIEWS)) {
return buildViews(session, catalog, filters);
}
if (table.equals(TABLE_SCHEMATA)) {
return buildSchemata(session, catalog);
}
if (table.equals(TABLE_INTERNAL_PARTITIONS)) {
return buildPartitions(session, catalog, filters);
}
if (table.equals(TABLE_TABLE_PRIVILEGES)) {
return buildTablePrivileges(session, catalog, filters);
}
throw new IllegalArgumentException(format("table does not exist: %s", table));
}
private InternalTable buildColumns(Session session, String catalogName, Map<String, NullableValue> filters)
{
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_COLUMNS));
QualifiedTablePrefix prefix = extractQualifiedTablePrefix(catalogName, filters);
for (Entry<SchemaTableName, List<ColumnMetadata>> entry : listTableColumns(session, metadata, accessControl, prefix).entrySet()) {
SchemaTableName tableName = entry.getKey();
int ordinalPosition = 1;
for (ColumnMetadata column : entry.getValue()) {
if (column.isHidden()) {
continue;
}
table.add(
catalogName,
tableName.getSchemaName(),
tableName.getTableName(),
column.getName(),
ordinalPosition,
null,
"YES",
column.getType().getDisplayName(),
column.getComment(),
column.getExtraInfo());
ordinalPosition++;
}
}
return table.build();
}
private InternalTable buildTables(Session session, String catalogName, Map<String, NullableValue> filters)
{
QualifiedTablePrefix prefix = extractQualifiedTablePrefix(catalogName, filters);
Set<SchemaTableName> tables = listTables(session, metadata, accessControl, prefix);
Set<SchemaTableName> views = listViews(session, metadata, accessControl, prefix);
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_TABLES));
for (SchemaTableName name : union(tables, views)) {
// if table and view names overlap, the view wins
String type = views.contains(name) ? "VIEW" : "BASE TABLE";
table.add(
catalogName,
name.getSchemaName(),
name.getTableName(),
type);
}
return table.build();
}
private InternalTable buildTablePrivileges(Session session, String catalogName, Map<String, NullableValue> filters)
{
QualifiedTablePrefix prefix = extractQualifiedTablePrefix(catalogName, filters);
List<GrantInfo> grants = ImmutableList.copyOf(listTablePrivileges(session, metadata, accessControl, prefix));
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_TABLE_PRIVILEGES));
for (GrantInfo grant : grants) {
for (PrivilegeInfo privilegeInfo : grant.getPrivilegeInfo()) {
table.add(
grant.getGrantor().orElse(null),
grant.getIdentity().getUser(),
catalogName,
grant.getSchemaTableName().getSchemaName(),
grant.getSchemaTableName().getTableName(),
privilegeInfo.getPrivilege().name(),
privilegeInfo.isGrantOption(),
grant.getWithHierarchy().orElse(null));
}
}
return table.build();
}
private InternalTable buildViews(Session session, String catalogName, Map<String, NullableValue> filters)
{
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_VIEWS));
for (Entry<QualifiedObjectName, ViewDefinition> entry : getViews(session, catalogName, filters).entrySet()) {
table.add(
entry.getKey().getCatalogName(),
entry.getKey().getSchemaName(),
entry.getKey().getObjectName(),
entry.getValue().getOriginalSql());
}
return table.build();
}
private Map<QualifiedObjectName, ViewDefinition> getViews(Session session, String catalogName, Map<String, NullableValue> filters)
{
return metadata.getViews(session, extractQualifiedTablePrefix(catalogName, filters));
}
private InternalTable buildSchemata(Session session, String catalogName)
{
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_SCHEMATA));
for (String schema : listSchemas(session, metadata, accessControl, catalogName)) {
table.add(catalogName, schema);
}
return table.build();
}
private InternalTable buildPartitions(Session session, String catalogName, Map<String, NullableValue> filters)
{
QualifiedObjectName tableName = extractQualifiedTableName(catalogName, filters);
InternalTable.Builder table = InternalTable.builder(informationSchemaTableColumns(TABLE_INTERNAL_PARTITIONS));
Optional<TableHandle> tableHandle = metadata.getTableHandle(session, tableName);
if (!tableHandle.isPresent()) {
throw new TableNotFoundException(tableName.asSchemaTableName());
}
List<TableLayoutResult> layouts = metadata.getLayouts(session, tableHandle.get(), Constraint.alwaysTrue(), Optional.empty());
if (layouts.size() == 1) {
Map<ColumnHandle, String> columnHandles = ImmutableBiMap.copyOf(metadata.getColumnHandles(session, tableHandle.get())).inverse();
Map<ColumnHandle, MethodHandle> methodHandles = new HashMap<>();
for (ColumnHandle columnHandle : columnHandles.keySet()) {
try {
ColumnMetadata columnMetadata = metadata.getColumnMetadata(session, tableHandle.get(), columnHandle);
Signature operator = metadata.getFunctionRegistry().getCoercion(columnMetadata.getType(), createUnboundedVarcharType());
MethodHandle methodHandle = metadata.getFunctionRegistry().getScalarFunctionImplementation(operator).getMethodHandle();
methodHandles.put(columnHandle, methodHandle);
}
catch (OperatorNotFoundException exception) {
// Do not put the columnHandle in the map.
}
}
TableLayout layout = Iterables.getOnlyElement(layouts).getLayout();
layout.getDiscretePredicates().ifPresent(predicates -> {
int partitionNumber = 1;
for (TupleDomain<ColumnHandle> domain : predicates.getPredicates()) {
for (Entry<ColumnHandle, NullableValue> entry : TupleDomain.extractFixedValues(domain).get().entrySet()) {
ColumnHandle columnHandle = entry.getKey();
String columnName = columnHandles.get(columnHandle);
String value = null;
if (entry.getValue().getValue() != null) {
if (methodHandles.containsKey(columnHandle)) {
try {
value = ((Slice) methodHandles.get(columnHandle).invokeWithArguments(entry.getValue().getValue())).toStringUtf8();
}
catch (Throwable throwable) {
throw Throwables.propagate(throwable);
}
}
else {
// OperatorNotFoundException was thrown for this columnHandle
value = "<UNREPRESENTABLE VALUE>";
}
}
table.add(
catalogName,
tableName.getSchemaName(),
tableName.getObjectName(),
partitionNumber,
columnName,
value);
}
partitionNumber++;
}
});
}
return table.build();
}
private static QualifiedObjectName extractQualifiedTableName(String catalogName, Map<String, NullableValue> filters)
{
Optional<String> schemaName = getFilterColumn(filters, "table_schema");
checkArgument(schemaName.isPresent(), "filter is required for column: %s.%s", TABLE_INTERNAL_PARTITIONS, "table_schema");
Optional<String> tableName = getFilterColumn(filters, "table_name");
checkArgument(tableName.isPresent(), "filter is required for column: %s.%s", TABLE_INTERNAL_PARTITIONS, "table_name");
return new QualifiedObjectName(catalogName, schemaName.get(), tableName.get());
}
private static QualifiedTablePrefix extractQualifiedTablePrefix(String catalogName, Map<String, NullableValue> filters)
{
Optional<String> schemaName = getFilterColumn(filters, "table_schema");
Optional<String> tableName = getFilterColumn(filters, "table_name");
if (!schemaName.isPresent()) {
return new QualifiedTablePrefix(catalogName, Optional.empty(), Optional.empty());
}
return new QualifiedTablePrefix(catalogName, schemaName, tableName);
}
private static Optional<String> getFilterColumn(Map<String, NullableValue> filters, String columnName)
{
NullableValue value = filters.get(columnName);
if (value == null || value.getValue() == null) {
return Optional.empty();
}
if (isVarcharType(value.getType())) {
return Optional.of(((Slice) value.getValue()).toStringUtf8().toLowerCase(ENGLISH));
}
return Optional.empty();
}
}