blob: f564d23413a6458558f0e8e1ac423e511e5ba0b2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.druid.sql.calcite.schema;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.apache.calcite.DataContext;
import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.Linq4j;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.RelDataTypeSystem;
import org.apache.calcite.schema.ScannableTable;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Statistic;
import org.apache.calcite.schema.Statistics;
import org.apache.calcite.schema.Table;
import org.apache.calcite.schema.TableMacro;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.server.security.Action;
import org.apache.druid.server.security.AuthenticationResult;
import org.apache.druid.server.security.AuthorizationUtils;
import org.apache.druid.server.security.AuthorizerMapper;
import org.apache.druid.server.security.Resource;
import org.apache.druid.server.security.ResourceAction;
import org.apache.druid.sql.calcite.planner.Calcites;
import org.apache.druid.sql.calcite.planner.DruidOperatorTable;
import org.apache.druid.sql.calcite.planner.DruidTypeSystem;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.table.DruidTable;
import org.apache.druid.sql.calcite.table.RowSignatures;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class InformationSchema extends AbstractSchema
{
private static final EmittingLogger log = new EmittingLogger(InformationSchema.class);
private static final String CATALOG_NAME = "druid";
private static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA";
private static final String SCHEMATA_TABLE = "SCHEMATA";
private static final String TABLES_TABLE = "TABLES";
private static final String COLUMNS_TABLE = "COLUMNS";
private static final String ROUTINES_TABLE = "ROUTINES";
private static class RowTypeBuilder
{
final RelDataTypeFactory typeFactory = DruidTypeSystem.TYPE_FACTORY;
final RelDataTypeFactory.Builder builder = typeFactory.builder();
public RowTypeBuilder add(String name, SqlTypeName type)
{
builder.add(name, Calcites.createSqlTypeWithNullability(typeFactory, type, false));
return this;
}
public RowTypeBuilder add(String name, SqlTypeName type, boolean nullable)
{
builder.add(name, Calcites.createSqlTypeWithNullability(typeFactory, type, nullable));
return this;
}
public RelDataType build()
{
return builder.build();
}
}
private static final RelDataType SCHEMATA_SIGNATURE = new RowTypeBuilder()
.add("CATALOG_NAME", SqlTypeName.VARCHAR)
.add("SCHEMA_NAME", SqlTypeName.VARCHAR)
.add("SCHEMA_OWNER", SqlTypeName.VARCHAR)
.add("DEFAULT_CHARACTER_SET_CATALOG", SqlTypeName.VARCHAR)
.add("DEFAULT_CHARACTER_SET_SCHEMA", SqlTypeName.VARCHAR)
.add("DEFAULT_CHARACTER_SET_NAME", SqlTypeName.VARCHAR)
.add("SQL_PATH", SqlTypeName.VARCHAR)
.build();
private static final RelDataType TABLES_SIGNATURE = new RowTypeBuilder()
.add("TABLE_CATALOG", SqlTypeName.VARCHAR)
.add("TABLE_SCHEMA", SqlTypeName.VARCHAR)
.add("TABLE_NAME", SqlTypeName.VARCHAR)
.add("TABLE_TYPE", SqlTypeName.VARCHAR)
.add("IS_JOINABLE", SqlTypeName.VARCHAR)
.add("IS_BROADCAST", SqlTypeName.VARCHAR)
.build();
private static final RelDataType COLUMNS_SIGNATURE = new RowTypeBuilder()
.add("TABLE_CATALOG", SqlTypeName.VARCHAR)
.add("TABLE_SCHEMA", SqlTypeName.VARCHAR)
.add("TABLE_NAME", SqlTypeName.VARCHAR)
.add("COLUMN_NAME", SqlTypeName.VARCHAR)
.add("ORDINAL_POSITION", SqlTypeName.BIGINT)
.add("COLUMN_DEFAULT", SqlTypeName.VARCHAR)
.add("IS_NULLABLE", SqlTypeName.VARCHAR)
.add("DATA_TYPE", SqlTypeName.VARCHAR)
.add("CHARACTER_MAXIMUM_LENGTH", SqlTypeName.VARCHAR, true)
.add("CHARACTER_OCTET_LENGTH", SqlTypeName.VARCHAR, true)
.add("NUMERIC_PRECISION", SqlTypeName.BIGINT, true)
.add("NUMERIC_PRECISION_RADIX", SqlTypeName.BIGINT, true)
.add("NUMERIC_SCALE", SqlTypeName.BIGINT, true)
.add("DATETIME_PRECISION", SqlTypeName.BIGINT, true)
.add("CHARACTER_SET_NAME", SqlTypeName.VARCHAR, true)
.add("COLLATION_NAME", SqlTypeName.VARCHAR, true)
.add("JDBC_TYPE", SqlTypeName.BIGINT)
.build();
private static final RelDataType ROUTINES_SIGNATURE = new RowTypeBuilder()
.add("ROUTINE_CATALOG", SqlTypeName.VARCHAR)
.add("ROUTINE_SCHEMA", SqlTypeName.VARCHAR)
.add("ROUTINE_NAME", SqlTypeName.VARCHAR)
.add("ROUTINE_TYPE", SqlTypeName.VARCHAR)
.add("IS_AGGREGATOR", SqlTypeName.VARCHAR)
.add("SIGNATURES", SqlTypeName.VARCHAR, true)
.build();
private static final RelDataTypeSystem TYPE_SYSTEM = RelDataTypeSystem.DEFAULT;
private static final String INFO_TRUE = "YES";
private static final String INFO_FALSE = "NO";
private final DruidSchemaCatalog rootSchema;
private final Map<String, Table> tableMap;
private final AuthorizerMapper authorizerMapper;
@Inject
public InformationSchema(
@Named(DruidCalciteSchemaModule.INCOMPLETE_SCHEMA) final DruidSchemaCatalog rootSchema,
final AuthorizerMapper authorizerMapper,
final DruidOperatorTable operatorTable
)
{
this.rootSchema = Preconditions.checkNotNull(rootSchema, "rootSchema");
this.tableMap = ImmutableMap.of(
SCHEMATA_TABLE, new SchemataTable(),
TABLES_TABLE, new TablesTable(),
COLUMNS_TABLE, new ColumnsTable(),
ROUTINES_TABLE, new RoutinesTable(operatorTable)
);
this.authorizerMapper = authorizerMapper;
}
@Override
protected Map<String, Table> getTableMap()
{
return tableMap;
}
class SchemataTable extends AbstractTable implements ScannableTable
{
@Override
public Enumerable<Object[]> scan(final DataContext root)
{
final FluentIterable<Object[]> results = FluentIterable
.from(rootSchema.getSubSchemaNames())
.transform(
new Function<String, Object[]>()
{
@Override
public Object[] apply(final String schemaName)
{
final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName);
return new Object[]{
CATALOG_NAME, // CATALOG_NAME
subSchema.getName(), // SCHEMA_NAME
null, // SCHEMA_OWNER
null, // DEFAULT_CHARACTER_SET_CATALOG
null, // DEFAULT_CHARACTER_SET_SCHEMA
null, // DEFAULT_CHARACTER_SET_NAME
null // SQL_PATH
};
}
}
);
return Linq4j.asEnumerable(results);
}
@Override
public RelDataType getRowType(final RelDataTypeFactory typeFactory)
{
return SCHEMATA_SIGNATURE;
}
@Override
public Statistic getStatistic()
{
return Statistics.UNKNOWN;
}
@Override
public TableType getJdbcTableType()
{
return TableType.SYSTEM_TABLE;
}
}
class TablesTable extends AbstractTable implements ScannableTable
{
@Override
public Enumerable<Object[]> scan(final DataContext root)
{
final FluentIterable<Object[]> results = FluentIterable
.from(rootSchema.getSubSchemaNames())
.transformAndConcat(
new Function<String, Iterable<Object[]>>()
{
@Override
public Iterable<Object[]> apply(final String schemaName)
{
final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName);
final AuthenticationResult authenticationResult =
(AuthenticationResult) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT);
final Set<String> authorizedTableNames = getAuthorizedTableNamesFromSubSchema(
subSchema,
authenticationResult
);
final Set<String> authorizedFunctionNames = getAuthorizedFunctionNamesFromSubSchema(
subSchema,
authenticationResult
);
return Iterables.filter(
Iterables.concat(
FluentIterable.from(authorizedTableNames).transform(
tableName -> {
final Table table = subSchema.getTable(tableName);
final boolean isJoinable;
final boolean isBroadcast;
if (table instanceof DruidTable) {
DruidTable druidTable = (DruidTable) table;
isJoinable = druidTable.isJoinable();
isBroadcast = druidTable.isBroadcast();
} else {
isJoinable = false;
isBroadcast = false;
}
return new Object[]{
CATALOG_NAME, // TABLE_CATALOG
schemaName, // TABLE_SCHEMA
tableName, // TABLE_NAME
table.getJdbcTableType().toString(), // TABLE_TYPE
isJoinable ? INFO_TRUE : INFO_FALSE, // IS_JOINABLE
isBroadcast ? INFO_TRUE : INFO_FALSE // IS_BROADCAST
};
}
),
FluentIterable.from(authorizedFunctionNames).transform(
new Function<String, Object[]>()
{
@Override
public Object[] apply(final String functionName)
{
if (getView(subSchema, functionName) != null) {
return new Object[]{
CATALOG_NAME, // TABLE_CATALOG
schemaName, // TABLE_SCHEMA
functionName, // TABLE_NAME
"VIEW", // TABLE_TYPE
INFO_FALSE, // IS_JOINABLE
INFO_FALSE // IS_BROADCAST
};
} else {
return null;
}
}
}
)
),
Predicates.notNull()
);
}
}
);
return Linq4j.asEnumerable(results);
}
@Override
public RelDataType getRowType(final RelDataTypeFactory typeFactory)
{
return TABLES_SIGNATURE;
}
@Override
public Statistic getStatistic()
{
return Statistics.UNKNOWN;
}
@Override
public TableType getJdbcTableType()
{
return TableType.SYSTEM_TABLE;
}
}
class ColumnsTable extends AbstractTable implements ScannableTable
{
@Override
public Enumerable<Object[]> scan(final DataContext root)
{
final FluentIterable<Object[]> results = FluentIterable
.from(rootSchema.getSubSchemaNames())
.transformAndConcat(
new Function<String, Iterable<Object[]>>()
{
@Override
public Iterable<Object[]> apply(final String schemaName)
{
final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName);
final RelDataTypeFactory typeFactory = root.getTypeFactory();
final AuthenticationResult authenticationResult =
(AuthenticationResult) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT);
final Set<String> authorizedTableNames = getAuthorizedTableNamesFromSubSchema(
subSchema,
authenticationResult
);
final Set<String> authorizedFunctionNames = getAuthorizedFunctionNamesFromSubSchema(
subSchema,
authenticationResult
);
return Iterables.concat(
Iterables.filter(
Iterables.concat(
FluentIterable.from(authorizedTableNames).transform(
new Function<String, Iterable<Object[]>>()
{
@Override
public Iterable<Object[]> apply(final String tableName)
{
Table table = subSchema.getTable(tableName);
if (table == null) {
// Table just disappeared.
return null;
}
return generateColumnMetadata(
schemaName,
tableName,
table.getRowType(typeFactory),
typeFactory
);
}
}
),
FluentIterable.from(authorizedFunctionNames).transform(
new Function<String, Iterable<Object[]>>()
{
@Override
public Iterable<Object[]> apply(final String functionName)
{
final TableMacro viewMacro = getView(subSchema, functionName);
if (viewMacro != null) {
try {
return generateColumnMetadata(
schemaName,
functionName,
viewMacro.apply(Collections.emptyList()).getRowType(typeFactory),
typeFactory
);
}
catch (Exception e) {
log.error(e, "Encountered exception while handling view[%s].", functionName);
return null;
}
} else {
return null;
}
}
}
)
),
Predicates.notNull()
)
);
}
}
);
return Linq4j.asEnumerable(results);
}
@Override
public RelDataType getRowType(final RelDataTypeFactory typeFactory)
{
return COLUMNS_SIGNATURE;
}
@Override
public Statistic getStatistic()
{
return Statistics.UNKNOWN;
}
@Override
public TableType getJdbcTableType()
{
return TableType.SYSTEM_TABLE;
}
@Nullable
private Iterable<Object[]> generateColumnMetadata(
final String schemaName,
final String tableName,
final RelDataType tableSchema,
final RelDataTypeFactory typeFactory
)
{
return FluentIterable
.from(tableSchema.getFieldList())
.transform(
new Function<RelDataTypeField, Object[]>()
{
@Override
public Object[] apply(final RelDataTypeField field)
{
final RelDataType type = field.getType();
SqlTypeName sqlTypeName = type.getSqlTypeName();
boolean isNumeric = SqlTypeName.NUMERIC_TYPES.contains(sqlTypeName);
boolean isCharacter = SqlTypeName.CHAR_TYPES.contains(sqlTypeName);
boolean isDateTime = SqlTypeName.DATETIME_TYPES.contains(sqlTypeName);
final String typeName = type instanceof RowSignatures.ComplexSqlType ? ((RowSignatures.ComplexSqlType) type).asTypeString() : sqlTypeName.toString();
return new Object[]{
CATALOG_NAME, // TABLE_CATALOG
schemaName, // TABLE_SCHEMA
tableName, // TABLE_NAME
field.getName(), // COLUMN_NAME
(long) (field.getIndex() + 1), // ORDINAL_POSITION
"", // COLUMN_DEFAULT
type.isNullable() ? INFO_TRUE : INFO_FALSE, // IS_NULLABLE
typeName, // DATA_TYPE
null, // CHARACTER_MAXIMUM_LENGTH
null, // CHARACTER_OCTET_LENGTH
isNumeric ? (long) type.getPrecision() : null, // NUMERIC_PRECISION
isNumeric ? 10L : null, // NUMERIC_PRECISION_RADIX
isNumeric ? (long) type.getScale() : null, // NUMERIC_SCALE
isDateTime ? (long) type.getPrecision() : null, // DATETIME_PRECISION
isCharacter ? type.getCharset().name() : null, // CHARACTER_SET_NAME
isCharacter ? type.getCollation().getCollationName() : null, // COLLATION_NAME
(long) type.getSqlTypeName().getJdbcOrdinal() // JDBC_TYPE (Druid extension)
};
}
}
);
}
}
static class RoutinesTable extends AbstractTable implements ScannableTable
{
private static final String FUNCTION = "FUNCTION";
private final DruidOperatorTable operatorTable;
public RoutinesTable(
DruidOperatorTable operatorTable
)
{
this.operatorTable = operatorTable;
}
@Override
public RelDataType getRowType(RelDataTypeFactory typeFactory)
{
return ROUTINES_SIGNATURE;
}
@Override
public Statistic getStatistic()
{
return Statistics.UNKNOWN;
}
@Override
public TableType getJdbcTableType()
{
return TableType.SYSTEM_TABLE;
}
@Override
public Enumerable<Object[]> scan(DataContext root)
{
final List<Object[]> rows = new ArrayList<>();
List<SqlOperator> operatorList = operatorTable.getOperatorList();
for (SqlOperator sqlOperator : operatorList) {
// For the ROUTINES table, we skip anything that's not a function syntax.
if (!DruidOperatorTable.isFunctionSyntax(sqlOperator.getSyntax())) {
continue;
}
Object[] row = new Object[]{
CATALOG_NAME, // ROUTINE_CATALOG
INFORMATION_SCHEMA_NAME, // ROUTINE_SCHEMA
sqlOperator.getName(), // ROUTINE_NAME
FUNCTION, // ROUTINE_TYPE
sqlOperator.isAggregator() ? INFO_TRUE : INFO_FALSE, // IS_AGGREGATOR
sqlOperator.getOperandTypeChecker() == null ? null : sqlOperator.getAllowedSignatures() // SIGNATURES
};
rows.add(row);
}
return Linq4j.asEnumerable(rows);
}
}
/**
* Return a view macro that may or may not be defined in a certain schema. If it's not defined, returns null.
*
* @param schemaPlus schema
* @param functionName function name
*
* @return view, or null
*/
@Nullable
private static TableMacro getView(final SchemaPlus schemaPlus, final String functionName)
{
// Look for a zero-arg function that is also a TableMacro. The returned value
// is never null so we don't need to check for that.
final Collection<org.apache.calcite.schema.Function> functions =
schemaPlus.getFunctions(functionName);
for (org.apache.calcite.schema.Function function : functions) {
if (function.getParameters().isEmpty() && function instanceof TableMacro) {
return (TableMacro) function;
}
}
return null;
}
private Set<String> getAuthorizedTableNamesFromSubSchema(
final SchemaPlus subSchema,
final AuthenticationResult authenticationResult
)
{
return getAuthorizedNamesFromNamedSchema(
authenticationResult,
rootSchema.getNamedSchema(subSchema.getName()),
subSchema.getTableNames()
);
}
private Set<String> getAuthorizedFunctionNamesFromSubSchema(
final SchemaPlus subSchema,
final AuthenticationResult authenticationResult
)
{
return getAuthorizedNamesFromNamedSchema(
authenticationResult,
rootSchema.getNamedSchema(subSchema.getName()),
subSchema.getFunctionNames()
);
}
private Set<String> getAuthorizedNamesFromNamedSchema(
final AuthenticationResult authenticationResult,
final NamedSchema schema,
final Set<String> names
)
{
if (schema == null) {
// for schemas with no resource type, or that are not named schemas, we don't filter anything
return names;
}
return ImmutableSet.copyOf(
AuthorizationUtils.filterAuthorizedResources(
authenticationResult,
names,
name -> {
final String resourseType = schema.getSchemaResourceType(name);
if (resourseType == null) {
return Collections.emptyList();
}
return Collections.singletonList(
new ResourceAction(new Resource(name, resourseType), Action.READ)
);
},
authorizerMapper
)
);
}
}