blob: 1fafb231237db229794d4161dd4431bf567a0bba [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.calcite.schema;
import org.apache.calcite.DataContext;
import org.apache.calcite.DataContexts;
import org.apache.calcite.adapter.enumerable.EnumUtils;
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.config.CalciteConnectionConfig;
import org.apache.calcite.config.CalciteConnectionConfigImpl;
import org.apache.calcite.config.CalciteConnectionProperty;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.jdbc.CalcitePrepare;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.linq4j.Enumerable;
import org.apache.calcite.linq4j.QueryProvider;
import org.apache.calcite.linq4j.Queryable;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.linq4j.tree.Expressions;
import org.apache.calcite.linq4j.tree.MethodCallExpression;
import org.apache.calcite.materialize.Lattice;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelProtoDataType;
import org.apache.calcite.runtime.ImmutablePairList;
import org.apache.calcite.runtime.PairList;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.tools.RelRunner;
import org.apache.calcite.util.BuiltInMethod;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.calcite.jdbc.CalciteSchema.LatticeEntry;
import static java.util.Objects.requireNonNull;
/**
* Utility functions for schemas.
*/
public final class Schemas {
private Schemas() {
throw new AssertionError("no instances!");
}
public static CalciteSchema.@Nullable FunctionEntry resolve(
RelDataTypeFactory typeFactory,
String name,
Collection<CalciteSchema.FunctionEntry> functionEntries,
List<RelDataType> argumentTypes) {
final List<CalciteSchema.FunctionEntry> matches = new ArrayList<>();
for (CalciteSchema.FunctionEntry entry : functionEntries) {
if (matches(typeFactory, entry.getFunction(), argumentTypes)) {
matches.add(entry);
}
}
switch (matches.size()) {
case 0:
return null;
case 1:
return matches.get(0);
default:
throw new RuntimeException("More than one match for " + name
+ " with arguments " + argumentTypes);
}
}
private static boolean matches(RelDataTypeFactory typeFactory,
Function member, List<RelDataType> argumentTypes) {
List<FunctionParameter> parameters = member.getParameters();
if (parameters.size() != argumentTypes.size()) {
return false;
}
for (int i = 0; i < argumentTypes.size(); i++) {
RelDataType argumentType = argumentTypes.get(i);
FunctionParameter parameter = parameters.get(i);
if (!canConvert(argumentType, parameter.getType(typeFactory))) {
return false;
}
}
return true;
}
private static boolean canConvert(RelDataType fromType, RelDataType toType) {
return SqlTypeUtil.canAssignFrom(toType, fromType);
}
/** Returns the expression for a schema. */
public static Expression expression(SchemaPlus schema) {
return schema.getExpression(schema.getParentSchema(), schema.getName());
}
/** Returns the expression for a sub-schema. */
public static Expression subSchemaExpression(SchemaPlus schema, String name,
Class type) {
// (Type) schemaExpression.getSubSchema("name")
final Expression schemaExpression = expression(schema);
Expression call =
Expressions.call(
schemaExpression,
BuiltInMethod.SCHEMA_GET_SUB_SCHEMA.method,
Expressions.constant(name));
//CHECKSTYLE: IGNORE 2
//noinspection unchecked
if (false && type != null && !type.isAssignableFrom(Schema.class)) {
return unwrap(call, type);
}
return call;
}
/** Converts a schema expression to a given type by calling the
* {@link SchemaPlus#unwrap(Class)} method. */
public static Expression unwrap(Expression call, Class type) {
return Expressions.convert_(
Expressions.call(call, BuiltInMethod.SCHEMA_PLUS_UNWRAP.method,
Expressions.constant(type)),
type);
}
/** Returns the expression to access a table within a schema. */
public static Expression tableExpression(SchemaPlus schema, Type elementType,
String tableName, Class clazz) {
final MethodCallExpression expression;
if (Table.class.isAssignableFrom(clazz)) {
expression =
Expressions.call(expression(schema),
BuiltInMethod.SCHEMA_GET_TABLE.method,
Expressions.constant(tableName));
if (ScannableTable.class.isAssignableFrom(clazz)) {
return Expressions.call(
BuiltInMethod.SCHEMAS_ENUMERABLE_SCANNABLE.method,
Expressions.convert_(expression, ScannableTable.class),
DataContext.ROOT);
}
if (FilterableTable.class.isAssignableFrom(clazz)) {
return Expressions.call(
BuiltInMethod.SCHEMAS_ENUMERABLE_FILTERABLE.method,
Expressions.convert_(expression, FilterableTable.class),
DataContext.ROOT);
}
if (ProjectableFilterableTable.class.isAssignableFrom(clazz)) {
return Expressions.call(
BuiltInMethod.SCHEMAS_ENUMERABLE_PROJECTABLE_FILTERABLE.method,
Expressions.convert_(expression, ProjectableFilterableTable.class),
DataContext.ROOT);
}
} else {
expression =
Expressions.call(BuiltInMethod.SCHEMAS_QUERYABLE.method,
DataContext.ROOT,
expression(schema),
Expressions.constant(elementType),
Expressions.constant(tableName));
}
return EnumUtils.convert(expression, clazz);
}
/**
* Generates an expression with which table can be referenced in
* generated code.
*
* @param schema Schema
* @param tableName Table name (unique within schema)
* @param table Table to be referenced
* @param clazz Class that provides specific methods for accessing table data.
* It may differ from the {@code table} class; for example {@code clazz} may be
* {@code MongoTable.MongoQueryable}, though {@code table} is {@code MongoTable}
*/
public static Expression getTableExpression(SchemaPlus schema, String tableName,
Table table, Class<?> clazz) {
if (table instanceof QueryableTable) {
QueryableTable queryableTable = (QueryableTable) table;
return queryableTable.getExpression(schema, tableName, clazz);
} else if (table instanceof ScannableTable
|| table instanceof FilterableTable
|| table instanceof ProjectableFilterableTable) {
return tableExpression(schema, Object[].class, tableName,
table.getClass());
} else if (table instanceof StreamableTable) {
return getTableExpression(schema, tableName,
((StreamableTable) table).stream(), clazz);
} else {
throw new UnsupportedOperationException();
}
}
public static DataContext createDataContext(
Connection connection, @Nullable SchemaPlus rootSchema) {
return DataContexts.of((CalciteConnection) connection, rootSchema);
}
/** Returns a {@link Queryable}, given a fully-qualified table name. */
public static <E> Queryable<E> queryable(DataContext root, Class<E> clazz,
String... names) {
return queryable(root, clazz, Arrays.asList(names));
}
/** Returns a {@link Queryable}, given a fully-qualified table name as an
* iterable. */
public static <E> Queryable<E> queryable(DataContext root, Class<E> clazz,
Iterable<? extends String> names) {
SchemaPlus schema = root.getRootSchema();
for (Iterator<? extends String> iterator = names.iterator();;) {
String name = iterator.next();
requireNonNull(schema, "schema");
if (iterator.hasNext()) {
SchemaPlus next = schema.getSubSchema(name);
if (next == null) {
throw new IllegalArgumentException("schema " + name + " is not found in " + schema);
}
schema = next;
} else {
return queryable(root, schema, clazz, name);
}
}
}
/** Returns a {@link Queryable}, given a schema and table name. */
public static <E> Queryable<E> queryable(DataContext root, SchemaPlus schema,
Class<E> clazz, String tableName) {
QueryableTable table =
(QueryableTable) requireNonNull(schema.getTable(tableName),
() -> "table " + tableName + " is not found in " + schema);
QueryProvider queryProvider = root.getQueryProvider();
return table.asQueryable(queryProvider, schema, tableName);
}
/** Returns an {@link org.apache.calcite.linq4j.Enumerable} over the rows of
* a given table, representing each row as an object array. */
public static Enumerable<@Nullable Object[]> enumerable(final ScannableTable table,
final DataContext root) {
return table.scan(root);
}
/** Returns an {@link org.apache.calcite.linq4j.Enumerable} over the rows of
* a given table, not applying any filters, representing each row as an object
* array. */
public static Enumerable<@Nullable Object[]> enumerable(final FilterableTable table,
final DataContext root) {
return table.scan(root, new ArrayList<>());
}
/** Returns an {@link org.apache.calcite.linq4j.Enumerable} over the rows of
* a given table, not applying any filters and projecting all columns,
* representing each row as an object array. */
public static Enumerable<@Nullable Object[]> enumerable(
final ProjectableFilterableTable table, final DataContext root) {
JavaTypeFactory typeFactory = root.getTypeFactory();
return table.scan(root, new ArrayList<>(),
identity(table.getRowType(typeFactory).getFieldCount()));
}
private static int[] identity(int count) {
final int[] integers = new int[count];
for (int i = 0; i < integers.length; i++) {
integers[i] = i;
}
return integers;
}
/** Returns an {@link org.apache.calcite.linq4j.Enumerable} over object
* arrays, given a fully-qualified table name which leads to a
* {@link ScannableTable}. */
public static @Nullable Table table(DataContext root, String... names) {
SchemaPlus schema = root.getRootSchema();
final List<String> nameList = Arrays.asList(names);
for (Iterator<? extends String> iterator = nameList.iterator();;) {
String name = iterator.next();
requireNonNull(schema, "schema");
if (iterator.hasNext()) {
SchemaPlus next = schema.getSubSchema(name);
if (next == null) {
throw new IllegalArgumentException("schema " + name + " is not found in " + schema);
}
schema = next;
} else {
return schema.getTable(name);
}
}
}
/** Parses and validates a SQL query. For use within Calcite only. */
public static CalcitePrepare.ParseResult parse(
final CalciteConnection connection, final CalciteSchema schema,
final @Nullable List<String> schemaPath, final String sql) {
final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
final ImmutableMap<CalciteConnectionProperty, String> propValues =
ImmutableMap.of();
final CalcitePrepare.Context context =
makeContext(connection, schema, schemaPath, null, propValues);
CalcitePrepare.Dummy.push(context);
try {
return prepare.parse(context, sql);
} finally {
CalcitePrepare.Dummy.pop(context);
}
}
/** Parses and validates a SQL query and converts to relational algebra. For
* use within Calcite only. */
public static CalcitePrepare.ConvertResult convert(
final CalciteConnection connection, final CalciteSchema schema,
final List<String> schemaPath, final String sql) {
final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
final ImmutableMap<CalciteConnectionProperty, String> propValues =
ImmutableMap.of();
final CalcitePrepare.Context context =
makeContext(connection, schema, schemaPath, null, propValues);
CalcitePrepare.Dummy.push(context);
try {
return prepare.convert(context, sql);
} finally {
CalcitePrepare.Dummy.pop(context);
}
}
/** Analyzes a view. For use within Calcite only. */
public static CalcitePrepare.AnalyzeViewResult analyzeView(
final CalciteConnection connection, final CalciteSchema schema,
final @Nullable List<String> schemaPath, final String viewSql,
@Nullable List<String> viewPath, boolean fail) {
final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
final ImmutableMap<CalciteConnectionProperty, String> propValues =
ImmutableMap.of();
final CalcitePrepare.Context context =
makeContext(connection, schema, schemaPath, viewPath, propValues);
CalcitePrepare.Dummy.push(context);
try {
return prepare.analyzeView(context, viewSql, fail);
} finally {
CalcitePrepare.Dummy.pop(context);
}
}
/** Prepares a SQL query for execution. For use within Calcite only. */
public static CalcitePrepare.CalciteSignature<Object> prepare(
final CalciteConnection connection, final CalciteSchema schema,
final @Nullable List<String> schemaPath, final String sql,
final ImmutableMap<CalciteConnectionProperty, String> map) {
final CalcitePrepare prepare = CalcitePrepare.DEFAULT_FACTORY.apply();
final CalcitePrepare.Context context =
makeContext(connection, schema, schemaPath, null, map);
CalcitePrepare.Dummy.push(context);
try {
return prepare.prepareSql(context, CalcitePrepare.Query.of(sql),
Object[].class, -1);
} finally {
CalcitePrepare.Dummy.pop(context);
}
}
/**
* Creates a context for the purposes of preparing a statement.
*
* @param connection Connection
* @param schema Schema
* @param schemaPath Path wherein to look for functions
* @param objectPath Path of the object being analyzed (usually a view),
* or null
* @param propValues Connection properties
* @return Context
*/
private static CalcitePrepare.Context makeContext(
CalciteConnection connection, CalciteSchema schema,
@Nullable List<String> schemaPath, @Nullable List<String> objectPath,
final ImmutableMap<CalciteConnectionProperty, String> propValues) {
if (connection == null) {
final CalcitePrepare.Context context0 = CalcitePrepare.Dummy.peek();
final CalciteConnectionConfig config =
mutate(context0.config(), propValues);
return makeContext(config, context0.getTypeFactory(),
context0.getDataContext(), schema, schemaPath, objectPath);
} else {
final CalciteConnectionConfig config =
mutate(connection.config(), propValues);
return makeContext(config, connection.getTypeFactory(),
DataContexts.of(connection, schema.root().plus()), schema,
schemaPath, objectPath);
}
}
private static CalciteConnectionConfig mutate(CalciteConnectionConfig config,
ImmutableMap<CalciteConnectionProperty, String> propValues) {
for (Map.Entry<CalciteConnectionProperty, String> e
: propValues.entrySet()) {
config =
((CalciteConnectionConfigImpl) config).set(e.getKey(), e.getValue());
}
return config;
}
private static CalcitePrepare.Context makeContext(
final CalciteConnectionConfig connectionConfig,
final JavaTypeFactory typeFactory,
final DataContext dataContext,
final CalciteSchema schema,
final @Nullable List<String> schemaPath, final @Nullable List<String> objectPath_) {
final @Nullable ImmutableList<String> objectPath =
objectPath_ == null ? null : ImmutableList.copyOf(objectPath_);
return new CalcitePrepare.Context() {
@Override public JavaTypeFactory getTypeFactory() {
return typeFactory;
}
@Override public CalciteSchema getRootSchema() {
return schema.root();
}
@Override public CalciteSchema getMutableRootSchema() {
return getRootSchema();
}
@Override public List<String> getDefaultSchemaPath() {
// schemaPath is usually null. If specified, it overrides schema
// as the context within which the SQL is validated.
if (schemaPath == null) {
return schema.path(null);
}
return schemaPath;
}
@Override public @Nullable List<String> getObjectPath() {
return objectPath;
}
@Override public CalciteConnectionConfig config() {
return connectionConfig;
}
@Override public DataContext getDataContext() {
return dataContext;
}
@Override public RelRunner getRelRunner() {
throw new UnsupportedOperationException();
}
@Override public CalcitePrepare.SparkHandler spark() {
final boolean enable = config().spark();
return CalcitePrepare.Dummy.getSparkHandler(enable);
}
};
}
/** Returns an implementation of
* {@link RelProtoDataType}
* that asks a given table for its row type with a given type factory. */
public static RelProtoDataType proto(final Table table) {
return table::getRowType;
}
/** Returns an implementation of {@link RelProtoDataType}
* that asks a given scalar function for its return type with a given type
* factory. */
public static RelProtoDataType proto(final ScalarFunction function) {
return function::getReturnType;
}
/** Returns the star tables defined in a schema.
*
* @param schema Schema */
public static List<CalciteSchema.TableEntry> getStarTables(
CalciteSchema schema) {
final List<CalciteSchema.LatticeEntry> list = getLatticeEntries(schema);
return Util.transform(list, entry -> {
final CalciteSchema.TableEntry starTable =
requireNonNull(entry, "entry").getStarTable();
assert starTable.getTable().getJdbcTableType()
== Schema.TableType.STAR;
return entry.getStarTable();
});
}
/** Returns the lattices defined in a schema.
*
* @param schema Schema */
public static List<Lattice> getLattices(CalciteSchema schema) {
final List<CalciteSchema.LatticeEntry> list = getLatticeEntries(schema);
return Util.transform(list, LatticeEntry::getLattice);
}
/** Returns the lattices defined in a schema.
*
* @param schema Schema */
public static List<CalciteSchema.LatticeEntry> getLatticeEntries(
CalciteSchema schema) {
final List<LatticeEntry> list = new ArrayList<>();
gatherLattices(schema, list);
return list;
}
private static void gatherLattices(CalciteSchema schema,
List<CalciteSchema.LatticeEntry> list) {
list.addAll(schema.getLatticeMap().values());
for (CalciteSchema subSchema : schema.getSubSchemaMap().values()) {
gatherLattices(subSchema, list);
}
}
/** Returns a sub-schema of a given schema obtained by following a sequence
* of names.
*
* <p>The result is null if the initial schema is null or any sub-schema does
* not exist.
*/
public static @Nullable CalciteSchema subSchema(CalciteSchema schema,
Iterable<String> names) {
@Nullable CalciteSchema current = schema;
for (String string : names) {
if (current == null) {
return null;
}
current = current.getSubSchema(string, false);
}
return current;
}
/** Generates a table name that is unique within the given schema. */
public static String uniqueTableName(CalciteSchema schema, String base) {
String t = requireNonNull(base, "base");
for (int x = 0; schema.getTable(t, true) != null; x++) {
t = base + x;
}
return t;
}
/** Creates a path with a given list of names starting from a given root
* schema. */
public static Path path(CalciteSchema rootSchema, Iterable<String> names) {
final ImmutableList.Builder<Pair<String, Schema>> builder =
ImmutableList.builder();
Schema schema = rootSchema.plus();
final Iterator<String> iterator = names.iterator();
if (!iterator.hasNext()) {
return PathImpl.EMPTY;
}
if (!rootSchema.name.isEmpty()) {
// If path starts with the name of the root schema, ignore the first step
// in the path.
checkState(rootSchema.name.equals(iterator.next()));
}
for (;;) {
final String name = iterator.next();
builder.add(Pair.of(name, schema));
if (!iterator.hasNext()) {
return path(builder.build());
}
Schema next = schema.getSubSchema(name);
if (next == null) {
throw new IllegalArgumentException("schema " + name + " is not found in " + schema);
}
schema = next;
}
}
public static Path path(Iterable<? extends Map.Entry<String, Schema>> list) {
return new PathImpl(ImmutablePairList.copyOf(list));
}
/** Returns the path to get to a schema from its root. */
public static Path path(SchemaPlus schema) {
PairList<String, Schema> list = PairList.of();
for (SchemaPlus s = schema; s != null; s = s.getParentSchema()) {
list.add(s.getName(), s);
}
list.reverse();
return new PathImpl(list.immutable());
}
/** Implementation of {@link Path}. */
private static class PathImpl
extends AbstractList<Pair<String, Schema>> implements Path {
private final ImmutablePairList<String, Schema> pairs;
private static final PathImpl EMPTY =
new PathImpl(ImmutablePairList.of());
PathImpl(ImmutablePairList<String, Schema> pairs) {
this.pairs = pairs;
}
@Override public boolean equals(@Nullable Object o) {
return this == o
|| o instanceof PathImpl
&& pairs.equals(((PathImpl) o).pairs);
}
@Override public int hashCode() {
return pairs.hashCode();
}
@Override public Pair<String, Schema> get(int index) {
return Pair.of(pairs.get(index));
}
@Override public int size() {
return pairs.size();
}
@Override public Path parent() {
if (pairs.isEmpty()) {
throw new IllegalArgumentException("at root");
}
return new PathImpl(pairs.subList(0, pairs.size() - 1));
}
@Override public List<String> names() {
return Util.skip(pairs.leftList());
}
@Override public List<Schema> schemas() {
return pairs.rightList();
}
}
}