blob: 8af414daf75dc8ade5154938c64243cc78d23a8e [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.prepare;
import org.apache.calcite.DataContext;
import org.apache.calcite.adapter.java.JavaTypeFactory;
import org.apache.calcite.jdbc.CalcitePrepare;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.jdbc.CalciteSchema.LatticeEntry;
import org.apache.calcite.plan.Convention;
import org.apache.calcite.plan.RelOptLattice;
import org.apache.calcite.plan.RelOptMaterialization;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptSchema;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.logical.LogicalTableModify;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexExecutorImpl;
import org.apache.calcite.runtime.Bindable;
import org.apache.calcite.runtime.Hook;
import org.apache.calcite.runtime.Typed;
import org.apache.calcite.schema.impl.StarTable;
import org.apache.calcite.sql.SqlExplain;
import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorCatalogReader;
import org.apache.calcite.sql.validate.SqlValidatorTable;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.tools.Program;
import org.apache.calcite.tools.Programs;
import org.apache.calcite.util.Holder;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.TryThreadLocal;
import org.apache.calcite.util.trace.CalciteTimingTracer;
import org.apache.calcite.util.trace.CalciteTrace;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
/**
* Abstract base for classes that implement
* the process of preparing and executing SQL expressions.
*/
public abstract class Prepare {
protected static final Logger LOGGER = CalciteTrace.getStatementTracer();
protected final CalcitePrepare.Context context;
protected final CatalogReader catalogReader;
protected String queryString = null;
/**
* Convention via which results should be returned by execution.
*/
protected final Convention resultConvention;
protected CalciteTimingTracer timingTracer;
protected List<List<String>> fieldOrigins;
protected RelDataType parameterRowType;
// temporary. for testing.
public static final TryThreadLocal<Boolean> THREAD_TRIM =
TryThreadLocal.of(false);
/** Temporary, until
* <a href="https://issues.apache.org/jira/browse/CALCITE-1045">[CALCITE-1045]
* Decorrelate sub-queries in Project and Join</a> is fixed.
*
* <p>The default is false, meaning do not expand queries during sql-to-rel,
* but a few tests override and set it to true. After CALCITE-1045
* is fixed, remove those overrides and use false everywhere. */
public static final TryThreadLocal<Boolean> THREAD_EXPAND =
TryThreadLocal.of(false);
public Prepare(CalcitePrepare.Context context, CatalogReader catalogReader,
Convention resultConvention) {
assert context != null;
this.context = context;
this.catalogReader = catalogReader;
this.resultConvention = resultConvention;
}
protected abstract PreparedResult createPreparedExplanation(
RelDataType resultType,
RelDataType parameterRowType,
RelRoot root,
boolean explainAsXml,
SqlExplainLevel detailLevel);
/**
* Optimizes a query plan.
*
* @param root Root of relational expression tree
* @param materializations Tables known to be populated with a given query
* @param lattices Lattices
* @return an equivalent optimized relational expression
*/
protected RelRoot optimize(final RelRoot root,
final List<Materialization> materializations,
final List<CalciteSchema.LatticeEntry> lattices) {
final RelOptPlanner planner = root.rel.getCluster().getPlanner();
planner.setRoot(root.rel);
final RelTraitSet desiredTraits = getDesiredRootTraitSet(root);
final Program program = getProgram();
final DataContext dataContext = context.getDataContext();
planner.setExecutor(new RexExecutorImpl(dataContext));
for (Materialization materialization : materializations) {
planner.addMaterialization(
new RelOptMaterialization(materialization.tableRel,
materialization.queryRel,
materialization.starRelOptTable));
}
for (CalciteSchema.LatticeEntry lattice : lattices) {
final CalciteSchema.TableEntry starTable = lattice.getStarTable();
final JavaTypeFactory typeFactory = context.getTypeFactory();
final RelOptTableImpl starRelOptTable =
RelOptTableImpl.create(catalogReader,
starTable.getTable().getRowType(typeFactory), starTable, null);
planner.addLattice(
new RelOptLattice(lattice.getLattice(), starRelOptTable));
}
final RelNode rootRel4 = program.run(planner, root.rel, desiredTraits);
LOGGER.debug("Plan after physical tweaks: {}",
RelOptUtil.toString(rootRel4, SqlExplainLevel.ALL_ATTRIBUTES));
return root.withRel(rootRel4);
}
private Program getProgram() {
// Allow a test to override the planner.
final List<Materialization> materializations = ImmutableList.of();
final Holder<Program> holder = Holder.of(null);
Hook.PROGRAM.run(Pair.of(materializations, holder));
if (holder.get() != null) {
return holder.get();
}
return Programs.standard();
}
protected RelTraitSet getDesiredRootTraitSet(RelRoot root) {
// Make sure non-CallingConvention traits, if any, are preserved
return root.rel.getTraitSet()
.replace(resultConvention)
.replace(root.collation)
.simplify();
}
/**
* Implements a physical query plan.
*
* @param root Root of the relational expression tree
* @return an executable plan
*/
protected abstract PreparedResult implement(RelRoot root);
public PreparedResult prepareSql(
SqlNode sqlQuery,
Class runtimeContextClass,
SqlValidator validator,
boolean needsValidation) {
return prepareSql(
sqlQuery,
sqlQuery,
runtimeContextClass,
validator,
needsValidation);
}
public PreparedResult prepareSql(
SqlNode sqlQuery,
SqlNode sqlNodeOriginal,
Class runtimeContextClass,
SqlValidator validator,
boolean needsValidation) {
queryString = sqlQuery.toString();
init(runtimeContextClass);
final SqlToRelConverter.ConfigBuilder builder =
SqlToRelConverter.configBuilder()
.withTrimUnusedFields(true)
.withExpand(THREAD_EXPAND.get())
.withExplain(sqlQuery.getKind() == SqlKind.EXPLAIN);
final SqlToRelConverter sqlToRelConverter =
getSqlToRelConverter(validator, catalogReader, builder.build());
SqlExplain sqlExplain = null;
if (sqlQuery.getKind() == SqlKind.EXPLAIN) {
// dig out the underlying SQL statement
sqlExplain = (SqlExplain) sqlQuery;
sqlQuery = sqlExplain.getExplicandum();
sqlToRelConverter.setDynamicParamCountInExplain(
sqlExplain.getDynamicParamCount());
}
RelRoot root =
sqlToRelConverter.convertQuery(sqlQuery, needsValidation, true);
Hook.CONVERTED.run(root.rel);
if (timingTracer != null) {
timingTracer.traceTime("end sql2rel");
}
final RelDataType resultType = validator.getValidatedNodeType(sqlQuery);
fieldOrigins = validator.getFieldOrigins(sqlQuery);
assert fieldOrigins.size() == resultType.getFieldCount();
parameterRowType = validator.getParameterRowType(sqlQuery);
// Display logical plans before view expansion, plugging in physical
// storage and decorrelation
if (sqlExplain != null) {
SqlExplain.Depth explainDepth = sqlExplain.getDepth();
boolean explainAsXml = sqlExplain.isXml();
SqlExplainLevel detailLevel = sqlExplain.getDetailLevel();
switch (explainDepth) {
case TYPE:
return createPreparedExplanation(
resultType, parameterRowType, null, explainAsXml, detailLevel);
case LOGICAL:
return createPreparedExplanation(
null, parameterRowType, root, explainAsXml, detailLevel);
default:
}
}
// Structured type flattening, view expansion, and plugging in physical
// storage.
root = root.withRel(flattenTypes(root.rel, true));
if (this.context.config().forceDecorrelate()) {
// Subquery decorrelation.
root = root.withRel(decorrelate(sqlToRelConverter, sqlQuery, root.rel));
}
// Trim unused fields.
root = trimUnusedFields(root);
Hook.TRIMMED.run(root.rel);
// Display physical plan after decorrelation.
if (sqlExplain != null) {
SqlExplain.Depth explainDepth = sqlExplain.getDepth();
boolean explainAsXml = sqlExplain.isXml();
SqlExplainLevel detailLevel = sqlExplain.getDetailLevel();
switch (explainDepth) {
case PHYSICAL:
default:
root = optimize(root, getMaterializations(), getLattices());
return createPreparedExplanation(
null, parameterRowType, root, explainAsXml, detailLevel);
}
}
root = optimize(root, getMaterializations(), getLattices());
if (timingTracer != null) {
timingTracer.traceTime("end optimization");
}
// For transformation from DML -> DML, use result of rewrite
// (e.g. UPDATE -> MERGE). For anything else (e.g. CALL -> SELECT),
// use original kind.
if (!root.kind.belongsTo(SqlKind.DML)) {
root = root.withKind(sqlNodeOriginal.getKind());
}
return implement(root);
}
protected LogicalTableModify.Operation mapTableModOp(
boolean isDml, SqlKind sqlKind) {
if (!isDml) {
return null;
}
switch (sqlKind) {
case INSERT:
return LogicalTableModify.Operation.INSERT;
case DELETE:
return LogicalTableModify.Operation.DELETE;
case MERGE:
return LogicalTableModify.Operation.MERGE;
case UPDATE:
return LogicalTableModify.Operation.UPDATE;
default:
return null;
}
}
/**
* Protected method to allow subclasses to override construction of
* SqlToRelConverter.
*/
protected abstract SqlToRelConverter getSqlToRelConverter(
SqlValidator validator,
CatalogReader catalogReader,
SqlToRelConverter.Config config);
public abstract RelNode flattenTypes(
RelNode rootRel,
boolean restructure);
protected abstract RelNode decorrelate(SqlToRelConverter sqlToRelConverter,
SqlNode query, RelNode rootRel);
protected abstract List<Materialization> getMaterializations();
protected abstract List<LatticeEntry> getLattices();
/**
* Walks over a tree of relational expressions, replacing each
* {@link org.apache.calcite.rel.RelNode} with a 'slimmed down' relational
* expression that projects
* only the columns required by its consumer.
*
* @param root Root of relational expression tree
* @return Trimmed relational expression
*/
protected RelRoot trimUnusedFields(RelRoot root) {
final SqlToRelConverter.Config config = SqlToRelConverter.configBuilder()
.withTrimUnusedFields(shouldTrim(root.rel))
.withExpand(THREAD_EXPAND.get())
.build();
final SqlToRelConverter converter =
getSqlToRelConverter(getSqlValidator(), catalogReader, config);
final boolean ordered = !root.collation.getFieldCollations().isEmpty();
final boolean dml = SqlKind.DML.contains(root.kind);
return root.withRel(converter.trimUnusedFields(dml || ordered, root.rel));
}
private boolean shouldTrim(RelNode rootRel) {
// For now, don't trim if there are more than 3 joins. The projects
// near the leaves created by trim migrate past joins and seem to
// prevent join-reordering.
return THREAD_TRIM.get() || RelOptUtil.countJoins(rootRel) < 2;
}
/**
* Returns a relational expression which is to be substituted for an access
* to a SQL view.
*
* @param rowType Row type of the view
* @param queryString Body of the view
* @param schemaPath List of schema names wherein to find referenced tables
* @return Relational expression
*/
public RelRoot expandView(RelDataType rowType, String queryString,
List<String> schemaPath) {
throw new UnsupportedOperationException();
}
protected abstract void init(Class runtimeContextClass);
protected abstract SqlValidator getSqlValidator();
/** Interface by which validator and planner can read table metadata. */
public interface CatalogReader
extends RelOptSchema, SqlValidatorCatalogReader, SqlOperatorTable {
PreparingTable getTableForMember(List<String> names);
/** Returns a catalog reader the same as this one but with a possibly
* different schema path. */
CatalogReader withSchemaPath(List<String> schemaPath);
PreparingTable getTable(List<String> names);
ThreadLocal<CatalogReader> THREAD_LOCAL = new ThreadLocal<>();
}
/** Definition of a table, for the purposes of the validator and planner. */
public interface PreparingTable
extends RelOptTable, SqlValidatorTable {
}
/**
* PreparedExplanation is a PreparedResult for an EXPLAIN PLAN statement.
* It's always good to have an explanation prepared.
*/
public abstract static class PreparedExplain
implements PreparedResult {
private final RelDataType rowType;
private final RelDataType parameterRowType;
private final RelRoot root;
private final boolean asXml;
private final SqlExplainLevel detailLevel;
public PreparedExplain(
RelDataType rowType,
RelDataType parameterRowType,
RelRoot root,
boolean asXml,
SqlExplainLevel detailLevel) {
this.rowType = rowType;
this.parameterRowType = parameterRowType;
this.root = root;
this.asXml = asXml;
this.detailLevel = detailLevel;
}
public String getCode() {
if (root == null) {
return RelOptUtil.dumpType(rowType);
} else {
return RelOptUtil.dumpPlan("", root.rel, asXml, detailLevel);
}
}
public RelDataType getParameterRowType() {
return parameterRowType;
}
public boolean isDml() {
return false;
}
public LogicalTableModify.Operation getTableModOp() {
return null;
}
public List<List<String>> getFieldOrigins() {
return Collections.singletonList(
Collections.<String>nCopies(4, null));
}
public abstract Bindable getBindable();
}
/**
* Result of a call to {@link Prepare#prepareSql}.
*/
public interface PreparedResult {
/**
* Returns the code generated by preparation.
*/
String getCode();
/**
* Returns whether this result is for a DML statement, in which case the
* result set is one row with one column containing the number of rows
* affected.
*/
boolean isDml();
/**
* Returns the table modification operation corresponding to this
* statement if it is a table modification statement; otherwise null.
*/
LogicalTableModify.Operation getTableModOp();
/**
* Returns a list describing, for each result field, the origin of the
* field as a 4-element list of (database, schema, table, column).
*/
List<List<String>> getFieldOrigins();
/**
* Returns a record type whose fields are the parameters of this statement.
*/
RelDataType getParameterRowType();
/**
* Executes the prepared result.
*
* @return producer of rows resulting from execution
*/
Bindable getBindable();
}
/**
* Abstract implementation of {@link PreparedResult}.
*/
public abstract static class PreparedResultImpl
implements PreparedResult, Typed {
protected final RelNode rootRel;
protected final RelDataType parameterRowType;
protected final RelDataType rowType;
protected final boolean isDml;
protected final LogicalTableModify.Operation tableModOp;
protected final List<List<String>> fieldOrigins;
protected final List<RelCollation> collations;
public PreparedResultImpl(
RelDataType rowType,
RelDataType parameterRowType,
List<List<String>> fieldOrigins,
List<RelCollation> collations,
RelNode rootRel,
LogicalTableModify.Operation tableModOp,
boolean isDml) {
this.rowType = Preconditions.checkNotNull(rowType);
this.parameterRowType = Preconditions.checkNotNull(parameterRowType);
this.fieldOrigins = Preconditions.checkNotNull(fieldOrigins);
this.collations = ImmutableList.copyOf(collations);
this.rootRel = Preconditions.checkNotNull(rootRel);
this.tableModOp = tableModOp;
this.isDml = isDml;
}
public boolean isDml() {
return isDml;
}
public LogicalTableModify.Operation getTableModOp() {
return tableModOp;
}
public List<List<String>> getFieldOrigins() {
return fieldOrigins;
}
public RelDataType getParameterRowType() {
return parameterRowType;
}
/**
* Returns the physical row type of this prepared statement. May not be
* identical to the row type returned by the validator; for example, the
* field names may have been made unique.
*/
public RelDataType getPhysicalRowType() {
return rowType;
}
public abstract Type getElementType();
public RelNode getRootRel() {
return rootRel;
}
public abstract Bindable getBindable();
}
/** Describes that a given SQL query is materialized by a given table.
* The materialization is currently valid, and can be used in the planning
* process. */
public static class Materialization {
/** The table that holds the materialized data. */
final CalciteSchema.TableEntry materializedTable;
/** The query that derives the data. */
final String sql;
/** The schema path for the query. */
final List<String> viewSchemaPath;
/** Relational expression for the table. Usually a
* {@link org.apache.calcite.rel.logical.LogicalTableScan}. */
RelNode tableRel;
/** Relational expression for the query to populate the table. */
RelNode queryRel;
/** Star table identified. */
private RelOptTable starRelOptTable;
public Materialization(CalciteSchema.TableEntry materializedTable,
String sql, List<String> viewSchemaPath) {
assert materializedTable != null;
assert sql != null;
this.materializedTable = materializedTable;
this.sql = sql;
this.viewSchemaPath = viewSchemaPath;
}
public void materialize(RelNode queryRel,
RelOptTable starRelOptTable) {
this.queryRel = queryRel;
this.starRelOptTable = starRelOptTable;
assert starRelOptTable.unwrap(StarTable.class) != null;
}
}
}
// End Prepare.java