http://git-wip-us.apache.org/repos/asf/calcite/blob/84b49f5b/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java new file mode 100644 index 0000000..fdc3774 --- /dev/null +++ b/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java @@ -0,0 +1,1283 @@ +/* + * 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.rel.rules; + +import org.apache.calcite.plan.RelOptMaterialization; +import org.apache.calcite.plan.RelOptMaterializations; +import org.apache.calcite.plan.RelOptPlanner; +import org.apache.calcite.plan.RelOptPredicateList; +import org.apache.calcite.plan.RelOptRule; +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.RelOptRuleOperand; +import org.apache.calcite.plan.RelOptTable; +import org.apache.calcite.plan.RelOptUtil; +import org.apache.calcite.plan.SubstitutionVisitor; +import org.apache.calcite.plan.volcano.VolcanoPlanner; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.Aggregate; +import org.apache.calcite.rel.core.AggregateCall; +import org.apache.calcite.rel.core.Filter; +import org.apache.calcite.rel.core.Join; +import org.apache.calcite.rel.core.JoinRelType; +import org.apache.calcite.rel.core.Project; +import org.apache.calcite.rel.core.RelFactories; +import org.apache.calcite.rel.core.TableScan; +import org.apache.calcite.rel.metadata.RelMetadataQuery; +import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexBuilder; +import org.apache.calcite.rex.RexCall; +import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexNode; +import org.apache.calcite.rex.RexShuttle; +import org.apache.calcite.rex.RexSimplify; +import org.apache.calcite.rex.RexTableInputRef; +import org.apache.calcite.rex.RexTableInputRef.RelTableRef; +import org.apache.calcite.rex.RexUtil; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.fun.SqlStdOperatorTable; +import org.apache.calcite.tools.RelBuilder; +import org.apache.calcite.tools.RelBuilder.AggCall; +import org.apache.calcite.tools.RelBuilderFactory; +import org.apache.calcite.util.ImmutableBitSet; +import org.apache.calcite.util.Util; +import org.apache.calcite.util.mapping.IntPair; +import org.apache.calcite.util.mapping.Mapping; +import org.apache.calcite.util.mapping.MappingType; +import org.apache.calcite.util.mapping.Mappings; +import org.apache.calcite.util.trace.CalciteLogger; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultiset; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; + +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Planner rule that converts a {@link org.apache.calcite.rel.core.Project} + * followed by {@link org.apache.calcite.rel.core.Aggregate} or an + * {@link org.apache.calcite.rel.core.Aggregate} to a scan (and possibly + * other operations) over a materialized view. + */ +public abstract class AbstractMaterializedViewRule extends RelOptRule { + + private static final CalciteLogger LOGGER = + new CalciteLogger(LoggerFactory.getLogger(AbstractMaterializedViewRule.class)); + + public static final MaterializedViewProjectJoinRule INSTANCE_PROJECT_JOIN = + new MaterializedViewProjectJoinRule(RelFactories.LOGICAL_BUILDER); + + public static final MaterializedViewOnlyJoinRule INSTANCE_JOIN = + new MaterializedViewOnlyJoinRule(RelFactories.LOGICAL_BUILDER); + + public static final MaterializedViewProjectAggregateRule INSTANCE_PROJECT_AGGREGATE = + new MaterializedViewProjectAggregateRule(RelFactories.LOGICAL_BUILDER); + + public static final MaterializedViewOnlyAggregateRule INSTANCE_AGGREGATE = + new MaterializedViewOnlyAggregateRule(RelFactories.LOGICAL_BUILDER); + + //~ Constructors ----------------------------------------------------------- + + /** Creates a AbstractMaterializedViewRule. */ + protected AbstractMaterializedViewRule(RelOptRuleOperand operand, + RelBuilderFactory relBuilderFactory, String description) { + super(operand, relBuilderFactory, description); + } + + /** + * Rewriting logic is based on "Optimizing Queries Using Materialized Views: + * A Practical, Scalable Solution" by Goldstein and Larson. + * + * On the query side, rules matches a Project-node chain or node, where node + * is either an Aggregate or a Join. Subplan rooted at the node operator must + * be composed of one or more of the following operators: TableScan, Project, + * Filter, and Join. + * + * For each join MV, we need to check the following: + * 1) The plan rooted at the Join operator in the view produces all rows + * needed by the plan rooted at the Join operator in the query. + * 2) All columns required by compensating predicates, i.e., predicates that + * need to be enforced over the view, are available at the view output. + * 3) All output expressions can be computed from the output of the view. + * 4) All output rows occur with the correct duplication factor. + * TODO: Currently we only allow the same tables in the view and the query, + * thus we are sure condition 4 is met. This restriction will be lifted in + * the future. + * + * In turn, for each aggregate MV, we need to check the following: + * 1) The plan rooted at the Aggregate operator in the view produces all rows + * needed by the plan rooted at the Aggregate operator in the query. + * 2) All columns required by compensating predicates, i.e., predicates that + * need to be enforced over the view, are available at the view output. + * 3) The grouping columns in the query are a subset of the grouping columns + * in the view. + * 4) All columns required to perform further grouping are available in the + * view output. + * 5) All columns required to compute output expressions are available in the + * view output. + */ + protected void perform(RelOptRuleCall call, Project topProject, RelNode node) { + final RexBuilder rexBuilder = node.getCluster().getRexBuilder(); + final RelMetadataQuery mq = RelMetadataQuery.instance(); + final RelOptPlanner planner = call.getPlanner(); + final RexSimplify simplify = + new RexSimplify(rexBuilder, true, + planner.getExecutor() != null ? planner.getExecutor() : RexUtil.EXECUTOR); + + final List<RelOptMaterialization> materializations = + (planner instanceof VolcanoPlanner) + ? ((VolcanoPlanner) planner).getMaterializations() + : ImmutableList.<RelOptMaterialization>of(); + + if (!materializations.isEmpty()) { + // 1. Explore query plan to recognize whether preconditions to + // try to generate a rewriting are met + if (!isValidPlan(topProject, node, mq)) { + return; + } + + // Obtain applicable (filtered) materializations + // TODO: Filtering of relevant materializations needs to be + // improved so we gather only materializations that might + // actually generate a valid rewriting. + final List<RelOptMaterialization> applicableMaterializations = + RelOptMaterializations.getApplicableMaterializations(node, materializations); + + if (!applicableMaterializations.isEmpty()) { + // 2. Initialize all query related auxiliary data structures + // that will be used throughout query rewriting process + final Multiset<RelOptTable> qTableBag = ImmutableMultiset.copyOf( + RelOptUtil.findAllTables(node)); + + // Generate query table references + final Set<RelTableRef> queryTableRefs = mq.getTableReferences(node); + if (queryTableRefs == null) { + // Bail out + return; + } + + // Extract query predicates + final RelOptPredicateList queryPredicateList = + mq.getAllPredicates(node); + if (queryPredicateList == null) { + // Bail out + return; + } + final RexNode[] queryPreds = splitPredicates( + rexBuilder, queryPredicateList.pulledUpPredicates); + + // Extract query equivalence classes. An equivalence class is a set + // of columns in the query output that are known to be equal. + final EquivalenceClasses qEC = new EquivalenceClasses(); + for (RexNode conj : RelOptUtil.conjunctions(queryPreds[0])) { + assert conj.isA(SqlKind.EQUALS); + RexCall equiCond = (RexCall) conj; + qEC.addEquivalenceClass( + (RexTableInputRef) equiCond.getOperands().get(0), + (RexTableInputRef) equiCond.getOperands().get(1)); + } + + // 3. We iterate through all applicable materializations trying to + // rewrite the given query + for (RelOptMaterialization materialization : applicableMaterializations) { + final Project topViewProject; + final RelNode viewNode; + // 3.1. Check whether it is a valid view + if (!isViewMatching(materialization.queryRel)) { + // Skip it + continue; + } + if (materialization.queryRel instanceof Project) { + topViewProject = (Project) materialization.queryRel; + viewNode = topViewProject.getInput(); + } else { + topViewProject = null; + viewNode = materialization.queryRel; + } + + // 3.2. View checks before proceeding + if (!isValidPlan(topViewProject, viewNode, mq)) { + // Skip it + continue; + } + + // 3.3. Initialize all query related auxiliary data structures + // that will be used throughout query rewriting process + // Extract view tables + Multiset<RelOptTable> vTableBag = ImmutableMultiset.copyOf( + RelOptUtil.findAllTables(viewNode)); + if (!qTableBag.equals(vTableBag)) { + // Currently we only support rewriting with views that use + // the same set of tables than the query, thus we skip it + // TODO: Extend to lift this restriction + continue; + } + + // Extract view predicates + final RelOptPredicateList viewPredicateList = + mq.getAllPredicates(viewNode); + if (viewPredicateList == null) { + // Skip it + continue; + } + final RexNode[] viewPreds = splitPredicates( + rexBuilder, viewPredicateList.pulledUpPredicates); + + // Extract view table references + final Set<RelTableRef> viewTableRefs = mq.getTableReferences(viewNode); + if (viewTableRefs == null) { + // Bail out + return; + } + + // 4. We map every table in the query to a view table with the same qualified + // name. + final Multimap<RelTableRef, RelTableRef> multiMapTables = ArrayListMultimap.create(); + for (RelTableRef queryTableRef : queryTableRefs) { + for (RelTableRef viewTableRef : viewTableRefs) { + if (queryTableRef.getQualifiedName().equals( + viewTableRef.getQualifiedName())) { + multiMapTables.put(queryTableRef, viewTableRef); + } + } + } + + // If a table is used multiple times, we will create multiple mappings, + // and we will try to rewrite the query using each of the mappings. + // Then, we will try to map every source table (query) to a target + // table (view), and if we are successful, we will try to create + // compensation predicates to filter the view results further + // (if needed). + final List<BiMap<RelTableRef, RelTableRef>> flatListMappings = + generateTableMappings(multiMapTables); + for (BiMap<RelTableRef, RelTableRef> tableMapping : flatListMappings) { + final RexNode compensationColumnsEquiPred; + final RexNode compensationRangePred; + final RexNode compensationResidualPred; + + // 4.1. Establish relationship between view and query equivalence classes. + // If every view equivalence class is not a subset of a query + // equivalence class, we bail out. + // To establish relationship, we swap column references of the view predicates + // to point to query tables. Then, we create the equivalence classes for the + // view predicates and check that every view equivalence class is a subset of a + // query equivalence class: if it is not, we bail out. + final RexNode viewColumnsEquiPred = RexUtil.swapTableReferences( + rexBuilder, viewPreds[0], tableMapping.inverse()); + final EquivalenceClasses vEC = new EquivalenceClasses(); + for (RexNode conj : RelOptUtil.conjunctions(viewColumnsEquiPred)) { + assert conj.isA(SqlKind.EQUALS); + RexCall equiCond = (RexCall) conj; + vEC.addEquivalenceClass( + (RexTableInputRef) equiCond.getOperands().get(0), + (RexTableInputRef) equiCond.getOperands().get(1)); + } + compensationColumnsEquiPred = generateEquivalenceClasses( + rexBuilder, queryPreds[0], qEC, viewColumnsEquiPred, vEC); + if (compensationColumnsEquiPred == null) { + // Skip it + continue; + } + + // 4.2. We check that range intervals for the query are contained in the view. + // Compute compensating predicates. + final RexNode queryRangePred = RexUtil.swapColumnReferences( + rexBuilder, queryPreds[1], qEC.getEquivalenceClassesMap()); + final RexNode viewRangePred = RexUtil.swapTableColumnReferences( + rexBuilder, viewPreds[1], tableMapping.inverse(), qEC.getEquivalenceClassesMap()); + compensationRangePred = SubstitutionVisitor.splitFilter( + simplify, queryRangePred, viewRangePred); + if (compensationRangePred == null) { + // Skip it + continue; + } + + // 4.3. Finally, we check that residual predicates of the query are satisfied + // within the view. + // Compute compensating predicates. + final RexNode queryResidualPred = RexUtil.swapColumnReferences( + rexBuilder, queryPreds[2], qEC.getEquivalenceClassesMap()); + final RexNode viewResidualPred = RexUtil.swapTableColumnReferences( + rexBuilder, viewPreds[2], tableMapping.inverse(), qEC.getEquivalenceClassesMap()); + compensationResidualPred = SubstitutionVisitor.splitFilter( + simplify, queryResidualPred, viewResidualPred); + if (compensationResidualPred == null) { + // Skip it + continue; + } + + // 4.4. Final compensation predicate. + RexNode compensationPred = RexUtil.composeConjunction( + rexBuilder, + ImmutableList.of( + compensationColumnsEquiPred, + compensationRangePred, + compensationResidualPred), + false); + if (!compensationPred.isAlwaysTrue()) { + // All columns required by compensating predicates must be contained + // in the view output (condition 2). + List<RexNode> viewExprs = extractExpressions(topViewProject, viewNode, + rexBuilder); + compensationPred = rewriteExpression(rexBuilder, viewNode, viewExprs, + compensationPred, tableMapping, qEC.getEquivalenceClassesMap(), mq); + if (compensationPred == null) { + // Skip it + continue; + } + } + + // 4.5. Generate final rewriting if possible. + // First, we add the compensation predicate (if any) on top of the view. + // Then, we trigger the Aggregate unifying method. This method will either create + // a Project or an Aggregate operator on top of the view. It will also compute the + // output expressions for the query. + RelBuilder builder = call.builder(); + builder.push(materialization.tableRel); + if (!compensationPred.isAlwaysTrue()) { + builder.filter(compensationPred); + } + RelNode result = unify(rexBuilder, builder, builder.build(), + topProject, node, topViewProject, viewNode, tableMapping, + qEC.getEquivalenceClassesMap(), mq); + if (result == null) { + // Skip it + continue; + } + call.transformTo(result); + } + } + } + } + } + + protected abstract boolean isValidPlan(Project topProject, RelNode node, + RelMetadataQuery mq); + + protected abstract boolean isViewMatching(RelNode node); + + protected abstract List<RexNode> extractExpressions(Project topProject, + RelNode node, RexBuilder rexBuilder); + + /* This method is responsible for rewriting the query using the given view query. + * + * The input node is a Scan on the view table and possibly a compensation Filter + * on top. If a rewriting can be produced, we return that rewriting. If it cannot + * be produced, we will return null. */ + protected abstract RelNode unify(RexBuilder rexBuilder, RelBuilder relBuilder, + RelNode input, Project topProject, RelNode node, + Project topViewProject, RelNode viewNode, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq); + + //~ Instances Join --------------------------------------------------------- + + /** Materialized view rewriting for join */ + private abstract static class MaterializedViewJoinRule + extends AbstractMaterializedViewRule { + /** Creates a MaterializedViewJoinRule. */ + protected MaterializedViewJoinRule(RelOptRuleOperand operand, + RelBuilderFactory relBuilderFactory, String description) { + super(operand, relBuilderFactory, description); + } + + @Override protected boolean isValidPlan(Project topProject, RelNode node, + RelMetadataQuery mq) { + Join join = (Join) node; + if (join.getJoinType() != JoinRelType.INNER) { + // TODO: Rewriting for non-inner joins not supported yet + return false; + } + return isValidRexNodePlan(join, mq); + } + + @Override protected boolean isViewMatching(RelNode node) { + if (node instanceof Join) { + return true; + } + if (node instanceof Project && ((Project) node).getInput() instanceof Join) { + return true; + } + return false; + } + + @Override protected List<RexNode> extractExpressions(Project topProject, + RelNode node, RexBuilder rexBuilder) { + Join viewJoin = (Join) node; + List<RexNode> viewExprs = new ArrayList<>(); + if (topProject != null) { + for (RexNode e : topProject.getChildExps()) { + viewExprs.add(e); + } + } else { + for (int i = 0; i < viewJoin.getRowType().getFieldCount(); i++) { + viewExprs.add(rexBuilder.makeInputRef(viewJoin, i)); + } + } + return viewExprs; + } + + @Override protected RelNode unify( + RexBuilder rexBuilder, + RelBuilder relBuilder, + RelNode input, + Project topProject, + RelNode node, + Project topViewProject, + RelNode viewNode, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq) { + List<RexNode> exprs = extractExpressions(topProject, node, rexBuilder); + List<RexNode> exprsLineage = new ArrayList<>(exprs.size()); + for (RexNode expr : exprs) { + Set<RexNode> s = mq.getExpressionLineage(node, expr); + if (s == null) { + // Bail out + return null; + } + assert s.size() == 1; + exprsLineage.add(s.iterator().next()); + } + List<RexNode> viewExprs = extractExpressions(topViewProject, viewNode, rexBuilder); + List<RexNode> rewrittenExprs = rewriteExpressions(rexBuilder, viewNode, viewExprs, + exprsLineage, tableMapping, equivalenceClassesMap, mq); + if (rewrittenExprs == null) { + return null; + } + return relBuilder + .push(input) + .project(rewrittenExprs) + .build(); + } + } + + /** Rule that matches Project on Join. */ + public static class MaterializedViewProjectJoinRule extends MaterializedViewJoinRule { + public MaterializedViewProjectJoinRule(RelBuilderFactory relBuilderFactory) { + super( + operand(Project.class, + operand(Join.class, any())), + relBuilderFactory, + "MaterializedViewJoinRule(Project-Join)"); + } + + @Override public void onMatch(RelOptRuleCall call) { + final Project project = call.rel(0); + final Join join = call.rel(1); + perform(call, project, join); + } + } + + /** Rule that matches Join. */ + public static class MaterializedViewOnlyJoinRule extends MaterializedViewJoinRule { + public MaterializedViewOnlyJoinRule(RelBuilderFactory relBuilderFactory) { + super( + operand(Join.class, any()), + relBuilderFactory, + "MaterializedViewJoinRule(Join)"); + } + + @Override public void onMatch(RelOptRuleCall call) { + final Join join = call.rel(0); + perform(call, null, join); + } + } + + //~ Instances Aggregate ---------------------------------------------------- + + /** Materialized view rewriting for aggregate */ + private abstract static class MaterializedViewAggregateRule + extends AbstractMaterializedViewRule { + /** Creates a MaterializedViewAggregateRule. */ + protected MaterializedViewAggregateRule(RelOptRuleOperand operand, + RelBuilderFactory relBuilderFactory, String description) { + super(operand, relBuilderFactory, description); + } + + @Override protected boolean isValidPlan(Project topProject, RelNode node, + RelMetadataQuery mq) { + Aggregate aggregate = (Aggregate) node; + if (aggregate.getGroupType() != Aggregate.Group.SIMPLE) { + // TODO: Rewriting with grouping sets not supported yet + return false; + } + return isValidRexNodePlan(aggregate.getInput(), mq); + } + + @Override protected boolean isViewMatching(RelNode node) { + if (node instanceof Aggregate) { + return true; + } + if (node instanceof Project && ((Project) node).getInput() instanceof Aggregate) { + return true; + } + return false; + } + + @Override protected List<RexNode> extractExpressions(Project topProject, + RelNode node, RexBuilder rexBuilder) { + Aggregate viewAggregate = (Aggregate) node; + List<RexNode> viewExprs = new ArrayList<>(); + if (topProject != null) { + for (RexNode e : topProject.getChildExps()) { + viewExprs.add(e); + } + } else { + for (int i = 0; i < viewAggregate.getGroupCount(); i++) { + viewExprs.add(rexBuilder.makeInputRef(viewAggregate, i)); + } + } + return viewExprs; + } + + @Override protected RelNode unify( + RexBuilder rexBuilder, + RelBuilder relBuilder, + RelNode input, + Project topProject, + RelNode node, + Project topViewProject, + RelNode viewNode, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq) { + final Aggregate queryAggregate = (Aggregate) node; + final Aggregate viewAggregate = (Aggregate) viewNode; + // Get group by references and aggregate call input references needed + ImmutableBitSet.Builder indexes = ImmutableBitSet.builder(); + ImmutableBitSet references = null; + if (topProject != null) { + // We have a Project on top, gather only what is needed + final RelOptUtil.InputFinder inputFinder = + new RelOptUtil.InputFinder(new LinkedHashSet<RelDataTypeField>()); + for (RexNode e : topProject.getChildExps()) { + e.accept(inputFinder); + } + references = inputFinder.inputBitSet.build(); + for (int i = 0; i < queryAggregate.getGroupCount(); i++) { + indexes.set(queryAggregate.getGroupSet().nth(i)); + } + for (int i = 0; i < queryAggregate.getAggCallList().size(); i++) { + if (references.get(queryAggregate.getGroupCount() + i)) { + for (int inputIdx : queryAggregate.getAggCallList().get(i).getArgList()) { + indexes.set(inputIdx); + } + } + } + } else { + // No project on top, all of them are needed + for (int i = 0; i < queryAggregate.getGroupCount(); i++) { + indexes.set(queryAggregate.getGroupSet().nth(i)); + } + for (AggregateCall queryAggCall : queryAggregate.getAggCallList()) { + for (int inputIdx : queryAggCall.getArgList()) { + indexes.set(inputIdx); + } + } + } + + // Create mapping from query columns to view columns + Multimap<Integer, Integer> m = generateMapping(rexBuilder, queryAggregate.getInput(), + viewAggregate.getInput(), indexes.build(), tableMapping, equivalenceClassesMap, mq); + if (m == null) { + // Bail out + return null; + } + + // We could map all expressions. Create aggregate mapping. + Mapping aggregateMapping = Mappings.create(MappingType.FUNCTION, + queryAggregate.getRowType().getFieldCount(), viewAggregate.getRowType().getFieldCount()); + for (int i = 0; i < queryAggregate.getGroupCount(); i++) { + Collection<Integer> c = m.get(queryAggregate.getGroupSet().nth(i)); + for (int j : c) { + int targetIdx = viewAggregate.getGroupSet().indexOf(j); + if (targetIdx == -1) { + continue; + } + aggregateMapping.set(i, targetIdx); + break; + } + if (aggregateMapping.getTargetOpt(i) == -1) { + // It is not part of group by, we bail out + return null; + } + } + for (int idx = 0; idx < queryAggregate.getAggCallList().size(); idx++) { + if (references != null && !references.get(queryAggregate.getGroupCount() + idx)) { + // Ignore + continue; + } + AggregateCall queryAggCall = queryAggregate.getAggCallList().get(idx); + List<Integer> queryAggCallIndexes = queryAggCall.getArgList(); + for (int j = 0; j < viewAggregate.getAggCallList().size(); j++) { + AggregateCall viewAggCall = viewAggregate.getAggCallList().get(j); + if (queryAggCall.getAggregation() != viewAggCall.getAggregation() + || queryAggCall.isDistinct() != viewAggCall.isDistinct() + || queryAggCall.getArgList().size() != viewAggCall.getArgList().size() + || queryAggCall.getType() != viewAggCall.getType()) { + // Continue + continue; + } + List<Integer> viewAggCallIndexes = new ArrayList<>(); + for (int aggCallIdx : viewAggCall.getArgList()) { + viewAggCallIndexes.add(m.get(aggCallIdx).iterator().next()); + } + if (!queryAggCallIndexes.equals(viewAggCallIndexes)) { + // Continue + continue; + } + aggregateMapping.set(queryAggregate.getGroupCount() + idx, + viewAggregate.getGroupCount() + j); + break; + } + } + + // Generate result rewriting + Mapping rewritingMapping = null; + RelNode result = relBuilder + .push(input) + .build(); + if (queryAggregate.getGroupCount() != viewAggregate.getGroupCount()) { + // Target is coarser level of aggregation. Generate an aggregate. + rewritingMapping = Mappings.create(MappingType.FUNCTION, + topViewProject != null ? topViewProject.getRowType().getFieldCount() + : viewAggregate.getRowType().getFieldCount(), + queryAggregate.getRowType().getFieldCount()); + final ImmutableBitSet.Builder groupSetB = ImmutableBitSet.builder(); + for (int i = 0; i < queryAggregate.getGroupCount(); i++) { + int targetIdx = aggregateMapping.getTargetOpt(i); + if (targetIdx == -1) { + // No matching group by column, we bail out + return null; + } + if (topViewProject != null) { + boolean added = false; + for (int k = 0; k < topViewProject.getChildExps().size(); k++) { + RexNode n = topViewProject.getChildExps().get(k); + if (!n.isA(SqlKind.INPUT_REF)) { + continue; + } + int ref = ((RexInputRef) n).getIndex(); + if (ref == targetIdx) { + groupSetB.set(k); + rewritingMapping.set(k, i); + added = true; + break; + } + } + if (!added) { + // No matching group by column, we bail out + return null; + } + } else { + groupSetB.set(targetIdx); + rewritingMapping.set(targetIdx, i); + } + } + final ImmutableBitSet groupSet = groupSetB.build(); + final List<AggCall> aggregateCalls = new ArrayList<>(); + for (int i = 0; i < queryAggregate.getAggCallList().size(); i++) { + if (references != null && !references.get(queryAggregate.getGroupCount() + i)) { + // Ignore + continue; + } + int sourceIdx = queryAggregate.getGroupCount() + i; + int targetIdx = aggregateMapping.getTargetOpt(sourceIdx); + if (targetIdx == -1) { + // No matching aggregation column, we bail out + return null; + } + AggregateCall queryAggCall = queryAggregate.getAggCallList().get(i); + if (topViewProject != null) { + boolean added = false; + for (int k = 0; k < topViewProject.getChildExps().size(); k++) { + RexNode n = topViewProject.getChildExps().get(k); + if (!n.isA(SqlKind.INPUT_REF)) { + continue; + } + int ref = ((RexInputRef) n).getIndex(); + if (ref == targetIdx) { + aggregateCalls.add( + relBuilder.aggregateCall( + SubstitutionVisitor.getRollup(queryAggCall.getAggregation()), + queryAggCall.isDistinct(), + null, + queryAggCall.name, + ImmutableList.of(rexBuilder.makeInputRef(input, k)))); + rewritingMapping.set(k, sourceIdx); + added = true; + break; + } + } + if (!added) { + // No matching aggregation column, we bail out + return null; + } + } else { + aggregateCalls.add( + relBuilder.aggregateCall( + SubstitutionVisitor.getRollup(queryAggCall.getAggregation()), + queryAggCall.isDistinct(), + null, + queryAggCall.name, + ImmutableList.of(rexBuilder.makeInputRef(input, targetIdx)))); + rewritingMapping.set(targetIdx, sourceIdx); + } + } + result = relBuilder + .push(result) + .aggregate(relBuilder.groupKey(groupSet, false, null), aggregateCalls) + .build(); + // We introduce a project on top, as group by columns order is lost + List<RexNode> projects = new ArrayList<>(); + Mapping inverseMapping = rewritingMapping.inverse(); + for (int i = 0; i < queryAggregate.getGroupCount(); i++) { + projects.add( + rexBuilder.makeInputRef(result, + groupSet.indexOf(inverseMapping.getTarget(i)))); + } + for (int i = 0; i < queryAggregate.getAggCallList().size(); i++) { + projects.add( + rexBuilder.makeInputRef(result, queryAggregate.getGroupCount() + i)); + } + result = relBuilder + .push(result) + .project(projects) + .build(); + } // end if queryAggregate.getGroupCount() != viewAggregate.getGroupCount() + + // Add query expressions on top. We first map query expressions to view + // expressions. Once we have done that, if the expression is contained + // and we have introduced already an operator on top of the input node, + // we use the mapping to resolve the position of the expression in the + // node. + final List<RexNode> topExprs = new ArrayList<>(); + if (topProject != null) { + topExprs.addAll(topProject.getChildExps()); + } else { + // Add all + for (int pos = 0; pos < queryAggregate.getRowType().getFieldCount(); pos++) { + topExprs.add(rexBuilder.makeInputRef(queryAggregate, pos)); + } + } + // Available in view. + final List<String> viewExprs = new ArrayList<>(); + if (topViewProject != null) { + for (int i = 0; i < topViewProject.getChildExps().size(); i++) { + viewExprs.add(topViewProject.getChildExps().get(i).toString()); + } + } else { + // Add all + for (int i = 0; i < viewAggregate.getRowType().getFieldCount(); i++) { + viewExprs.add(rexBuilder.makeInputRef(viewAggregate, i).toString()); + } + } + final List<RexNode> rewrittenExprs = new ArrayList<>(topExprs.size()); + for (RexNode expr : topExprs) { + RexNode rewrittenExpr = shuttleReferences(rexBuilder, expr, aggregateMapping); + if (rewrittenExpr == null) { + // Cannot map expression + return null; + } + int pos = viewExprs.indexOf(rewrittenExpr.toString()); + if (pos == -1) { + // Cannot map expression + return null; + } + if (rewritingMapping != null) { + pos = rewritingMapping.getTargetOpt(pos); + if (pos == -1) { + // Cannot map expression + return null; + } + } + rewrittenExprs.add(rexBuilder.makeInputRef(result, pos)); + } + return relBuilder + .push(result) + .project(rewrittenExprs) + .build(); + } + } + + /** Rule that matches Project on Aggregate. */ + public static class MaterializedViewProjectAggregateRule extends MaterializedViewAggregateRule { + public MaterializedViewProjectAggregateRule(RelBuilderFactory relBuilderFactory) { + super( + operand(Project.class, + operand(Aggregate.class, any())), + relBuilderFactory, + "MaterializedViewAggregateRule(Project-Aggregate)"); + } + + @Override public void onMatch(RelOptRuleCall call) { + final Project project = call.rel(0); + final Aggregate aggregate = call.rel(1); + perform(call, project, aggregate); + } + } + + /** Rule that matches Aggregate. */ + public static class MaterializedViewOnlyAggregateRule extends MaterializedViewAggregateRule { + public MaterializedViewOnlyAggregateRule(RelBuilderFactory relBuilderFactory) { + super( + operand(Aggregate.class, any()), + relBuilderFactory, + "MaterializedViewAggregateRule(Aggregate)"); + } + + @Override public void onMatch(RelOptRuleCall call) { + final Aggregate aggregate = call.rel(0); + perform(call, null, aggregate); + } + } + + //~ Methods ---------------------------------------------------------------- + + /* It will flatten a multimap containing table references to table references, + * producing all possible combinations of mappings. Each of the mappings will + * be bi-directional. */ + private static List<BiMap<RelTableRef, RelTableRef>> generateTableMappings( + Multimap<RelTableRef, RelTableRef> multiMapTables) { + final List<BiMap<RelTableRef, RelTableRef>> result = new ArrayList<>(); + if (multiMapTables.isEmpty()) { + return result; + } + result.add(HashBiMap.<RelTableRef, RelTableRef>create()); + for (Entry<RelTableRef, Collection<RelTableRef>> e : multiMapTables.asMap().entrySet()) { + boolean added = false; + for (RelTableRef target : e.getValue()) { + if (added) { + for (BiMap<RelTableRef, RelTableRef> m : result) { + final BiMap<RelTableRef, RelTableRef> newM = + HashBiMap.<RelTableRef, RelTableRef>create(m); + newM.put(e.getKey(), target); + result.add(newM); + } + } else { + for (BiMap<RelTableRef, RelTableRef> m : result) { + m.put(e.getKey(), target); + } + added = true; + } + } + // Mapping needs to exist + assert added; + } + return result; + } + + /* Currently we only support TableScan - Project - Filter - Join */ + private static boolean isValidRexNodePlan(RelNode node, RelMetadataQuery mq) { + final Multimap<Class<? extends RelNode>, RelNode> m = + mq.getNodeTypes(node); + for (Class<? extends RelNode> c : m.keySet()) { + if (!TableScan.class.isAssignableFrom(c) + && !Project.class.isAssignableFrom(c) + && !Filter.class.isAssignableFrom(c) + && !Join.class.isAssignableFrom(c)) { + // Skip it + return false; + } + } + return true; + } + + /* Classifies each of the predicates in the list into one of these three + * categories: + * - column equality predicates, or + * - range predicates, comprising <, <=, >, >=, and = between a reference + * and a constant, or + * - residual predicates, all the rest + * + * For each category, it creates the conjunction of the predicates. The + * result is an array of three RexNode objects corresponding to each + * category. */ + private static RexNode[] splitPredicates( + RexBuilder rexBuilder, ImmutableList<RexNode> predicates) { + List<RexNode> equiColumnsPreds = new ArrayList<>(); + List<RexNode> rangePreds = new ArrayList<>(); + List<RexNode> residualPreds = new ArrayList<>(); + for (RexNode e : predicates) { + switch (e.getKind()) { + case EQUALS: + RexCall eqCall = (RexCall) e; + if (RexUtil.isReferenceOrAccess(eqCall.getOperands().get(0), false) + && RexUtil.isReferenceOrAccess(eqCall.getOperands().get(1), false)) { + equiColumnsPreds.add(e); + } else if ((RexUtil.isReferenceOrAccess(eqCall.getOperands().get(0), false) + && RexUtil.isConstant(eqCall.getOperands().get(1))) + || (RexUtil.isReferenceOrAccess(eqCall.getOperands().get(1), false) + && RexUtil.isConstant(eqCall.getOperands().get(0)))) { + rangePreds.add(e); + } else { + residualPreds.add(e); + } + break; + case LESS_THAN: + case GREATER_THAN: + case LESS_THAN_OR_EQUAL: + case GREATER_THAN_OR_EQUAL: + case NOT_EQUALS: + RexCall rangeCall = (RexCall) e; + if ((RexUtil.isReferenceOrAccess(rangeCall.getOperands().get(0), false) + && RexUtil.isConstant(rangeCall.getOperands().get(1))) + || (RexUtil.isReferenceOrAccess(rangeCall.getOperands().get(1), false) + && RexUtil.isConstant(rangeCall.getOperands().get(0)))) { + rangePreds.add(e); + } else { + residualPreds.add(e); + } + break; + default: + residualPreds.add(e); + } + } + return new RexNode[] { + RexUtil.composeConjunction(rexBuilder, equiColumnsPreds, false), + RexUtil.composeConjunction(rexBuilder, rangePreds, false), + RexUtil.composeConjunction(rexBuilder, residualPreds, false)}; + } + + /* Given the equi-column predicates of the query and the view and the + * computed equivalence classes, it extracts possible mappings between + * the equivalence classes. + * + * If there is no mapping, it returns null. If there is a exact match, + * it will return a compensation predicate that evaluates to true. + * Finally, if a compensation predicate needs to be enforced on top of + * the view to make the equivalences classes match, it returns that + * compensation predicate */ + private static RexNode generateEquivalenceClasses(RexBuilder rexBuilder, + RexNode queryEquiColumnsPreds, EquivalenceClasses qEC, + RexNode viewEquiColumnsPreds, EquivalenceClasses vEC) { + if (queryEquiColumnsPreds.isAlwaysTrue() && viewEquiColumnsPreds.isAlwaysTrue()) { + // No column equality predicates in query and view + // Empty mapping and compensation predicate + assert qEC.getEquivalenceClasses().isEmpty() && vEC.getEquivalenceClasses().isEmpty(); + return rexBuilder.makeLiteral(true); + } + if (queryEquiColumnsPreds.isAlwaysTrue() || viewEquiColumnsPreds.isAlwaysTrue()) { + // No column equality predicates in query or view + assert qEC.getEquivalenceClasses().isEmpty() || vEC.getEquivalenceClasses().isEmpty(); + return null; + } + + final List<Set<RexTableInputRef>> queryEquivalenceClasses = qEC.getEquivalenceClasses(); + final List<Set<RexTableInputRef>> viewEquivalenceClasses = vEC.getEquivalenceClasses(); + final Mapping mapping = extractPossibleMapping( + queryEquivalenceClasses, viewEquivalenceClasses); + if (mapping == null) { + // Did not find mapping between the equivalence classes, + // bail out + return null; + } + + // Create the compensation predicate + RexNode compensationPredicate = rexBuilder.makeLiteral(true); + for (IntPair pair : mapping) { + Set<RexTableInputRef> difference = new HashSet<>( + queryEquivalenceClasses.get(pair.target)); + difference.removeAll(viewEquivalenceClasses.get(pair.source)); + for (RexTableInputRef e : difference) { + RexNode equals = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, + e, viewEquivalenceClasses.get(pair.source).iterator().next()); + compensationPredicate = rexBuilder.makeCall(SqlStdOperatorTable.AND, + compensationPredicate, equals); + } + } + + return compensationPredicate; + } + + /* Given the query and view equivalence classes, it extracts the possible mappings + * from each view equivalence class to each query equivalence class. + * + * If any of the view equivalence classes cannot be mapped to a query equivalence + * class, it returns null. */ + private static Mapping extractPossibleMapping( + List<Set<RexTableInputRef>> queryEquivalenceClasses, + List<Set<RexTableInputRef>> viewEquivalenceClasses) { + Mapping mapping = Mappings.create(MappingType.FUNCTION, + viewEquivalenceClasses.size(), queryEquivalenceClasses.size()); + for (int i = 0; i < viewEquivalenceClasses.size(); i++) { + boolean foundQueryEquivalenceClass = false; + final Set<RexTableInputRef> viewEquivalenceClass = viewEquivalenceClasses.get(i); + for (int j = 0; j < queryEquivalenceClasses.size(); j++) { + final Set<RexTableInputRef> queryEquivalenceClass = queryEquivalenceClasses.get(j); + if (queryEquivalenceClass.containsAll(viewEquivalenceClass)) { + mapping.set(i, j); + foundQueryEquivalenceClass = true; + break; + } + } // end for + + if (!foundQueryEquivalenceClass) { + // View equivalence class not found in query equivalence class + return null; + } + } // end for + + return mapping; + } + + /* Given the input expression that references source expressions in the query, + * it will rewrite it to refer to the view output. + * + * If any of the subexpressions in the input expression cannot be mapped to + * the query, it will return null. + */ + private static RexNode rewriteExpression( + RexBuilder rexBuilder, + RelNode viewNode, + List<RexNode> viewExprs, + RexNode expr, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq) { + List<RexNode> rewrittenExprs = rewriteExpressions(rexBuilder, viewNode, viewExprs, + ImmutableList.of(expr), tableMapping, equivalenceClassesMap, mq); + if (rewrittenExprs == null) { + return null; + } + assert rewrittenExprs.size() == 1; + return rewrittenExprs.get(0); + } + + private static List<RexNode> rewriteExpressions( + RexBuilder rexBuilder, + RelNode viewNode, + List<RexNode> viewExprs, + List<RexNode> exprs, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq) { + Map<String, Integer> exprsLineage = new HashMap<>(); + for (int i = 0; i < viewExprs.size(); i++) { + final RexNode e = viewExprs.get(i); + final Set<RexNode> s = mq.getExpressionLineage(viewNode, e); + if (s == null) { + // Next expression + continue; + } + // We only support project - filter - join, thus it should map to + // a single expression + assert s.size() == 1; + // Rewrite expr to be expressed on query tables + exprsLineage.put( + RexUtil.swapTableColumnReferences( + rexBuilder, + s.iterator().next(), + tableMapping.inverse(), + equivalenceClassesMap).toString(), + i); + } + + List<RexNode> rewrittenExprs = new ArrayList<>(exprs.size()); + for (RexNode expr : exprs) { + RexNode rewrittenExpr = replaceWithOriginalReferences(rexBuilder, expr, exprsLineage); + if (RexUtil.containsTableInputRef(rewrittenExpr) != null) { + // Some expressions were not present in view output + return null; + } + rewrittenExprs.add(rewrittenExpr); + } + return rewrittenExprs; + } + + /* Mapping from node expressions to target expressions. + * + * If any of the expressions cannot be mapped, we return null. */ + private static Multimap<Integer, Integer> generateMapping( + RexBuilder rexBuilder, + RelNode node, + RelNode target, + ImmutableBitSet positions, + BiMap<RelTableRef, RelTableRef> tableMapping, + Map<RexTableInputRef, Set<RexTableInputRef>> equivalenceClassesMap, + RelMetadataQuery mq) { + Multimap<String, Integer> exprsLineage = ArrayListMultimap.create(); + for (int i = 0; i < target.getRowType().getFieldCount(); i++) { + Set<RexNode> s = mq.getExpressionLineage(target, rexBuilder.makeInputRef(target, i)); + if (s == null) { + // Bail out + continue; + } + // We only support project - filter - join, thus it should map to + // a single expression + assert s.size() == 1; + // Rewrite expr to be expressed on query tables + exprsLineage.put( + RexUtil.swapTableColumnReferences( + rexBuilder, + s.iterator().next(), + tableMapping.inverse(), + equivalenceClassesMap).toString(), + i); + } + + Multimap<Integer, Integer> m = ArrayListMultimap.create(); + for (int i : positions) { + Set<RexNode> s = mq.getExpressionLineage(node, rexBuilder.makeInputRef(node, i)); + if (s == null) { + // Bail out + return null; + } + // We only support project - filter - join, thus it should map to + // a single expression + assert s.size() == 1; + // Rewrite expr to be expressed on query tables + Collection<Integer> c = exprsLineage.get( + RexUtil.swapColumnReferences( + rexBuilder, s.iterator().next(), equivalenceClassesMap).toString()); + if (c == null) { + // Bail out + return null; + } + for (Integer j : c) { + m.put(i, j); + } + } + return m; + } + + /* Given the input expression, it will replace (sub)expressions when possible + * using the content of the mapping. In particular, the mapping contains the + * digest of the expression and the index that the replacement input ref should + * point to. */ + private static RexNode replaceWithOriginalReferences(final RexBuilder rexBuilder, + final RexNode expr, final Map<String, Integer> mapping) { + // Currently we allow the following: + // 1) compensation pred can be directly map to expression + // 2) all references in compensation pred can be map to expressions + RexShuttle visitor = + new RexShuttle() { + @Override public RexNode visitCall(RexCall call) { + Integer pos = mapping.get(call.toString()); + if (pos != null) { + // Found it + return rexBuilder.makeInputRef(call.getType(), pos); + } + return super.visitCall(call); + } + + @Override public RexNode visitTableInputRef(RexTableInputRef inputRef) { + Integer pos = mapping.get(inputRef.toString()); + if (pos != null) { + // Found it + return rexBuilder.makeInputRef(inputRef.getType(), pos); + } + return super.visitTableInputRef(inputRef); + } + }; + return visitor.apply(expr); + } + + /* Replaces all the input references by the position in the + * input column set. If a reference index cannot be found in + * the input set, then we return null. */ + private static RexNode shuttleReferences(final RexBuilder rexBuilder, + final RexNode node, final Mapping mapping) { + try { + RexShuttle visitor = + new RexShuttle() { + @Override public RexNode visitInputRef(RexInputRef inputRef) { + int pos = mapping.getTargetOpt(inputRef.getIndex()); + if (pos != -1) { + // Found it + return rexBuilder.makeInputRef(inputRef.getType(), pos); + } + throw Util.FoundOne.NULL; + } + }; + return visitor.apply(node); + } catch (Util.FoundOne ex) { + Util.swallow(ex, null); + return null; + } + } + + /** + * Class representing an equivalence class, i.e., a set of equivalent columns. + */ + private static class EquivalenceClasses { + + private Map<RexTableInputRef, Set<RexTableInputRef>> nodeToEquivalenceClass; + + protected EquivalenceClasses() { + nodeToEquivalenceClass = new HashMap<>(); + } + + protected void addEquivalenceClass(RexTableInputRef p1, RexTableInputRef p2) { + Set<RexTableInputRef> c1 = nodeToEquivalenceClass.get(p1); + Set<RexTableInputRef> c2 = nodeToEquivalenceClass.get(p2); + if (c1 != null && c2 != null) { + // Both present, we need to merge + if (c1.size() < c2.size()) { + // We swap them to merge + c1 = c2; + p1 = p2; + } + for (RexTableInputRef newRef : c2) { + c1.add(newRef); + nodeToEquivalenceClass.put(newRef, c1); + } + } else if (c1 != null) { + // p1 present, we need to merge into it + c1.add(p2); + nodeToEquivalenceClass.put(p2, c1); + } else if (c2 != null) { + // p2 present, we need to merge into it + c2.add(p1); + nodeToEquivalenceClass.put(p1, c2); + } else { + // None are present, add to same equivalence class + Set<RexTableInputRef> equivalenceClass = new LinkedHashSet<>(); + equivalenceClass.add(p1); + equivalenceClass.add(p2); + nodeToEquivalenceClass.put(p1, equivalenceClass); + nodeToEquivalenceClass.put(p2, equivalenceClass); + } + } + + protected Map<RexTableInputRef, Set<RexTableInputRef>> getEquivalenceClassesMap() { + return ImmutableMap.copyOf(nodeToEquivalenceClass); + } + + protected List<Set<RexTableInputRef>> getEquivalenceClasses() { + return ImmutableList.copyOf(nodeToEquivalenceClass.values()); + } + } +} + +// End AbstractMaterializedViewRule.java
http://git-wip-us.apache.org/repos/asf/calcite/blob/84b49f5b/core/src/main/java/org/apache/calcite/rex/RexTableInputRef.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rex/RexTableInputRef.java b/core/src/main/java/org/apache/calcite/rex/RexTableInputRef.java index 7e2d26b..b986684 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexTableInputRef.java +++ b/core/src/main/java/org/apache/calcite/rex/RexTableInputRef.java @@ -16,18 +16,35 @@ */ package org.apache.calcite.rex; -import org.apache.calcite.rel.metadata.RelTableRef; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.sql.SqlKind; +import com.google.common.collect.ImmutableList; + +import java.util.List; + /** - * Variable which references a field of an input relational expression + * Variable which references a column of a table occurrence in a relational plan. + * + * <p>This object is used by: + * - {@link {@link org.apache.calcite.rel.metadata.BuiltInMetadata.ExpressionLineage}, and + * - {@link {@link org.apache.calcite.rel.metadata.BuiltInMetadata.AllPredicates}. + * + * <p>Given a relational expression, its purpose is to be able to reference uniquely + * the provenance of a given expression. For that, it uses a unique table reference + * (contained in a {@link RelTableRef}) and an column index within the table. + * + * <p>For example, {@code A.#0.$3 + 2} column {@code $3} in the {@code 0} occurrence of + * table {@code A} in the plan. + * + * Note that this kind of {@link RexNode} is an auxiliary data structure with a very + * specific purpose and should not be used in relational expressions. */ public class RexTableInputRef extends RexInputRef { private final RelTableRef tableRef; - public RexTableInputRef(RelTableRef tableRef, int index, RelDataType type) { + private RexTableInputRef(RelTableRef tableRef, int index, RelDataType type) { super(index, type); this.tableRef = tableRef; this.digest = tableRef.toString() + ".$" + index; @@ -50,12 +67,12 @@ public class RexTableInputRef extends RexInputRef { return tableRef; } - public String getQualifiedName() { + public List<String> getQualifiedName() { return tableRef.getQualifiedName(); } public int getIdentifier() { - return tableRef.getIdentifier(); + return tableRef.getEntityNumber(); } public static RexTableInputRef of(RelTableRef tableRef, int index, RelDataType type) { @@ -77,6 +94,49 @@ public class RexTableInputRef extends RexInputRef { @Override public SqlKind getKind() { return SqlKind.TABLE_INPUT_REF; } + + /** Identifies uniquely a table by its qualified name and its entity number (occurrence) */ + public static class RelTableRef { + + private final List<String> qualifiedName; + private final int entityNumber; + private final String digest; + + private RelTableRef(List<String> qualifiedName, int entityNumber) { + this.qualifiedName = ImmutableList.copyOf(qualifiedName); + this.entityNumber = entityNumber; + this.digest = qualifiedName + ".#" + entityNumber; + } + + //~ Methods ---------------------------------------------------------------- + + @Override public boolean equals(Object obj) { + return this == obj + || obj instanceof RelTableRef + && qualifiedName.equals(((RelTableRef) obj).qualifiedName) + && entityNumber == ((RelTableRef) obj).entityNumber; + } + + @Override public int hashCode() { + return digest.hashCode(); + } + + public List<String> getQualifiedName() { + return qualifiedName; + } + + public int getEntityNumber() { + return entityNumber; + } + + @Override public String toString() { + return digest; + } + + public static RelTableRef of(List<String> qualifiedName, int entityNumber) { + return new RelTableRef(qualifiedName, entityNumber); + } + } } // End RexTableInputRef.java http://git-wip-us.apache.org/repos/asf/calcite/blob/84b49f5b/core/src/main/java/org/apache/calcite/rex/RexUtil.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java b/core/src/main/java/org/apache/calcite/rex/RexUtil.java index 0af3fc7..95ab2c2 100644 --- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java +++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java @@ -24,11 +24,11 @@ import org.apache.calcite.rel.RelFieldCollation; import org.apache.calcite.rel.core.Filter; import org.apache.calcite.rel.core.Join; import org.apache.calcite.rel.core.Project; -import org.apache.calcite.rel.metadata.RelTableRef; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rel.type.RelDataTypeFamily; import org.apache.calcite.rel.type.RelDataTypeField; +import org.apache.calcite.rex.RexTableInputRef.RelTableRef; import org.apache.calcite.runtime.PredicateImpl; import org.apache.calcite.schema.Schemas; import org.apache.calcite.sql.SqlAggFunction; @@ -1867,16 +1867,31 @@ public class RexUtil { } } + /** + * Given an expression, it will swap the table references contained in its + * {@link RexTableInputRef} using the contents in the map. + */ public static RexNode swapTableReferences(final RexBuilder rexBuilder, final RexNode node, final Map<RelTableRef, RelTableRef> tableMapping) { return swapTableColumnReferences(rexBuilder, node, tableMapping, null); } + /** + * Given an expression, it will swap its column references {@link RexTableInputRef} + * using the contents in the map (in particular, the first element of the set in the + * map value). + */ public static RexNode swapColumnReferences(final RexBuilder rexBuilder, final RexNode node, final Map<RexTableInputRef, Set<RexTableInputRef>> ec) { return swapTableColumnReferences(rexBuilder, node, null, ec); } + /** + * Given an expression, it will swap the table references contained in its + * {@link RexTableInputRef} using the contents in the first map, and then + * it will swap the column references {@link RexTableInputRef} using the contents + * in the second map (in particular, the first element of the set in the map value). + */ public static RexNode swapTableColumnReferences(final RexBuilder rexBuilder, final RexNode node, final Map<RelTableRef, RelTableRef> tableMapping, final Map<RexTableInputRef, Set<RexTableInputRef>> ec) { @@ -1884,7 +1899,7 @@ public class RexUtil { new RexShuttle() { @Override public RexNode visitTableInputRef(RexTableInputRef inputRef) { if (tableMapping != null) { - inputRef = new RexTableInputRef( + inputRef = RexTableInputRef.of( tableMapping.get(inputRef.getTableRef()), inputRef.getIndex(), inputRef.getType()); http://git-wip-us.apache.org/repos/asf/calcite/blob/84b49f5b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java index df2f2f4..3265db5 100644 --- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java +++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java @@ -64,6 +64,7 @@ import org.apache.calcite.rel.metadata.BuiltInMetadata.Predicates; import org.apache.calcite.rel.metadata.BuiltInMetadata.RowCount; import org.apache.calcite.rel.metadata.BuiltInMetadata.Selectivity; import org.apache.calcite.rel.metadata.BuiltInMetadata.Size; +import org.apache.calcite.rel.metadata.BuiltInMetadata.TableReferences; import org.apache.calcite.rel.metadata.BuiltInMetadata.UniqueKeys; import org.apache.calcite.rel.metadata.Metadata; import org.apache.calcite.rex.RexNode; @@ -376,6 +377,7 @@ public enum BuiltInMethod { ImmutableBitSet.class), COLUMN_ORIGIN(ColumnOrigin.class, "getColumnOrigins", int.class), EXPRESSION_LINEAGE(ExpressionLineage.class, "getExpressionLineage", RexNode.class), + TABLE_REFERENCES(TableReferences.class, "getTableReferences"), CUMULATIVE_COST(CumulativeCost.class, "getCumulativeCost"), NON_CUMULATIVE_COST(NonCumulativeCost.class, "getNonCumulativeCost"), PREDICATES(Predicates.class, "getPredicates"), http://git-wip-us.apache.org/repos/asf/calcite/blob/84b49f5b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java index 4ad07d4..29603b1 100644 --- a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java +++ b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java @@ -60,6 +60,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -743,6 +744,8 @@ public class MaterializationTest { rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, x, i1); // $0 = 1 final RexNode x_eq_1_b = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, x, i1); // $0 = 1 again + final RexNode x_eq_2 = + rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, x, i2); // $0 = 2 final RexNode y_eq_2 = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, y, i2); // $1 = 2 final RexNode z_eq_3 = @@ -757,32 +760,32 @@ public class MaterializationTest { // condition: x = 1, // target: x = 1 or z = 3 // yields - // residue: not (z = 3) + // residue: x = 1 newFilter = SubstitutionVisitor.splitFilter(simplify, x_eq_1, rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, z_eq_3)); - assertThat(newFilter.toString(), equalTo("NOT(=($2, 3))")); + assertThat(newFilter.toString(), equalTo("=($0, 1)")); // 2b. // condition: x = 1 or y = 2 // target: x = 1 or y = 2 or z = 3 // yields - // residue: not (z = 3) + // residue: x = 1 or y = 2 newFilter = SubstitutionVisitor.splitFilter(simplify, rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2), rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2, z_eq_3)); - assertThat(newFilter.toString(), equalTo("NOT(=($2, 3))")); + assertThat(newFilter.toString(), equalTo("OR(=($0, 1), =($1, 2))")); // 2c. // condition: x = 1 // target: x = 1 or y = 2 or z = 3 // yields - // residue: not (y = 2) and not (z = 3) + // residue: x = 1 newFilter = SubstitutionVisitor.splitFilter(simplify, x_eq_1, rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2, z_eq_3)); assertThat(newFilter.toString(), - equalTo("AND(NOT(=($1, 2)), NOT(=($2, 3)))")); + equalTo("=($0, 1)")); // 2d. // condition: x = 1 or y = 2 @@ -807,16 +810,59 @@ public class MaterializationTest { // target: x = 1 // yields // residue: null - // TODO: + newFilter = SubstitutionVisitor.splitFilter(simplify, + rexBuilder.makeCall(SqlStdOperatorTable.OR, x_eq_1, y_eq_2), + x_eq_1); + assertNull(newFilter); // Example 3. // Condition [x = 1 and y = 2], // target [y = 2 and x = 1] yields // residue [true]. - // TODO: + newFilter = SubstitutionVisitor.splitFilter(simplify, + rexBuilder.makeCall(SqlStdOperatorTable.AND, x_eq_1, y_eq_2), + rexBuilder.makeCall(SqlStdOperatorTable.AND, y_eq_2, x_eq_1)); + assertThat(newFilter.isAlwaysTrue(), equalTo(true)); // Example 4. - // TODO: + // condition: x = 1 and y = 2 + // target: y = 2 + // yields + // residue: x = 1 + newFilter = SubstitutionVisitor.splitFilter(simplify, + rexBuilder.makeCall(SqlStdOperatorTable.AND, x_eq_1, y_eq_2), + y_eq_2); + assertThat(newFilter.toString(), equalTo("=($0, 1)")); + + // Example 5. + // condition: x = 1 + // target: x = 1 and y = 2 + // yields + // residue: null + newFilter = SubstitutionVisitor.splitFilter(simplify, + x_eq_1, + rexBuilder.makeCall(SqlStdOperatorTable.AND, x_eq_1, y_eq_2)); + assertNull(newFilter); + + // Example 6. + // condition: x = 1 + // target: y = 2 + // yields + // residue: null + newFilter = SubstitutionVisitor.splitFilter(simplify, + x_eq_1, + y_eq_2); + assertNull(newFilter); + + // Example 7. + // condition: x = 1 + // target: x = 2 + // yields + // residue: null + newFilter = SubstitutionVisitor.splitFilter(simplify, + x_eq_1, + x_eq_2); + assertNull(newFilter); } /** Tests a complicated star-join query on a complicated materialized @@ -924,6 +970,287 @@ public class MaterializationTest { "EnumerableTableScan(table=[[hr, m0]])", 1)); } + @Test public void testAggregateMaterializationNoAggregateFuncs1() { + checkMaterialize( + "select \"empid\", \"deptno\" from \"emps\" group by \"empid\", \"deptno\"", + "select \"empid\", \"deptno\" from \"emps\" group by \"empid\", \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs2() { + checkMaterialize( + "select \"empid\", \"deptno\" from \"emps\" group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs3() { + checkNoMaterialize( + "select \"deptno\" from \"emps\" group by \"deptno\"", + "select \"empid\", \"deptno\" from \"emps\" group by \"empid\", \"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs4() { + checkMaterialize( + "select \"empid\", \"deptno\" from \"emps\" where \"deptno\" = 10 group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" where \"deptno\" = 10 group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs5() { + checkNoMaterialize( + "select \"empid\", \"deptno\" from \"emps\" where \"deptno\" = 5 group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" where \"deptno\" = 10 group by \"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs6() { + checkMaterialize( + "select \"empid\", \"deptno\" from \"emps\" where \"deptno\" > 5 group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" where \"deptno\" > 10 group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}])\n" + + " EnumerableCalc(expr#0..1=[{inputs}], expr#2=[10], expr#3=[>($t1, $t2)], " + + "proj#0..1=[{exprs}], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs7() { + checkNoMaterialize( + "select \"empid\", \"deptno\" from \"emps\" where \"deptno\" > 5 group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" where \"deptno\" < 10 group by \"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs8() { + checkNoMaterialize( + "select \"empid\" from \"emps\" group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" group by \"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationNoAggregateFuncs9() { + checkNoMaterialize( + "select \"empid\", \"deptno\" from \"emps\"\n" + + "where \"salary\" > 1000 group by \"name\", \"empid\", \"deptno\"", + "select \"empid\" from \"emps\"\n" + + "where \"salary\" > 2000 group by \"name\", \"empid\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationAggregateFuncs1() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) as c, sum(\"empid\") as s\n" + + "from \"emps\" group by \"empid\", \"deptno\"", + "select \"deptno\" from \"emps\" group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationAggregateFuncs2() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) as c, sum(\"empid\") as s\n" + + "from \"emps\" group by \"empid\", \"deptno\"", + "select \"deptno\", count(*) as c, sum(\"empid\") as s\n" + + "from \"emps\" group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}], C=[$SUM0($2)], S=[$SUM0($3)])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationAggregateFuncs3() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) as c, sum(\"empid\") as s\n" + + "from \"emps\" group by \"empid\", \"deptno\"", + "select \"deptno\", \"empid\", sum(\"empid\") as s, count(*) as c\n" + + "from \"emps\" group by \"empid\", \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..3=[{inputs}], deptno=[$t1], empid=[$t0], " + + "S=[$t3], C=[$t2])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationAggregateFuncs4() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) as c, sum(\"empid\") as s\n" + + "from \"emps\" where \"deptno\" >= 10 group by \"empid\", \"deptno\"", + "select \"deptno\", sum(\"empid\") as s\n" + + "from \"emps\" where \"deptno\" > 10 group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{1}], S=[$SUM0($3)])\n" + + " EnumerableCalc(expr#0..3=[{inputs}], expr#4=[10], expr#5=[>($t1, $t4)], " + + "proj#0..3=[{exprs}], $condition=[$t5])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationAggregateFuncs5() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) + 1 as c, sum(\"empid\") as s\n" + + "from \"emps\" where \"deptno\" >= 10 group by \"empid\", \"deptno\"", + "select \"deptno\", sum(\"empid\") + 1 as s\n" + + "from \"emps\" where \"deptno\" > 10 group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[1], expr#3=[+($t1, $t2)], " + + "deptno=[$t0], S=[$t3])\n" + + " EnumerableAggregate(group=[{1}], agg#0=[$SUM0($3)])\n" + + " EnumerableCalc(expr#0..3=[{inputs}], expr#4=[10], expr#5=[>($t1, $t4)], " + + "proj#0..3=[{exprs}], $condition=[$t5])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testAggregateMaterializationAggregateFuncs6() { + checkNoMaterialize( + "select \"empid\", \"deptno\", count(*) + 1 as c, sum(\"empid\") + 2 as s\n" + + "from \"emps\" where \"deptno\" >= 10 group by \"empid\", \"deptno\"", + "select \"deptno\", sum(\"empid\") + 1 as s\n" + + "from \"emps\" where \"deptno\" > 10 group by \"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testAggregateMaterializationAggregateFuncs7() { + checkMaterialize( + "select \"empid\", \"deptno\", count(*) + 1 as c, sum(\"empid\") as s\n" + + "from \"emps\" where \"deptno\" >= 10 group by \"empid\", \"deptno\"", + "select \"deptno\" + 1, sum(\"empid\") + 1 as s\n" + + "from \"emps\" where \"deptno\" > 10 group by \"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[1], expr#3=[+($t0, $t2)], " + + "expr#4=[+($t1, $t2)], EXPR$0=[$t3], S=[$t4])\n" + + " EnumerableAggregate(group=[{1}], agg#0=[$SUM0($3)])\n" + + " EnumerableCalc(expr#0..3=[{inputs}], expr#4=[10], expr#5=[>($t1, $t4)], " + + "proj#0..3=[{exprs}], $condition=[$t5])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Ignore + @Test public void testAggregateMaterializationAggregateFuncs8() { + // TODO: It should work, but top project in the query is not matched by the planner. + // It needs further checking. + checkMaterialize( + "select \"empid\", \"deptno\" + 1, count(*) + 1 as c, sum(\"empid\") as s\n" + + "from \"emps\" where \"deptno\" >= 10 group by \"empid\", \"deptno\"", + "select \"deptno\" + 1, sum(\"empid\") + 1 as s\n" + + "from \"emps\" where \"deptno\" > 10 group by \"deptno\""); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs1() { + checkMaterialize( + "select \"empid\", \"depts\".\"deptno\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 10\n" + + "group by \"empid\", \"depts\".\"deptno\"", + "select \"empid\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 20\n" + + "group by \"empid\", \"depts\".\"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[20], expr#3=[>($t1, $t2)], " + + "empid=[$t0], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs2() { + checkMaterialize( + "select \"depts\".\"deptno\", \"empid\" from \"depts\"\n" + + "join \"emps\" using (\"deptno\") where \"depts\".\"deptno\" > 10\n" + + "group by \"empid\", \"depts\".\"deptno\"", + "select \"empid\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 20\n" + + "group by \"empid\", \"depts\".\"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[20], expr#3=[>($t0, $t2)], " + + "empid=[$t1], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs3() { + // It does not match, Project on top of query + checkNoMaterialize( + "select \"empid\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 10\n" + + "group by \"empid\", \"depts\".\"deptno\"", + "select \"empid\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 20\n" + + "group by \"empid\", \"depts\".\"deptno\"", + JdbcTest.HR_MODEL); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs4() { + checkMaterialize( + "select \"empid\", \"depts\".\"deptno\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"emps\".\"deptno\" > 10\n" + + "group by \"empid\", \"depts\".\"deptno\"", + "select \"empid\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"depts\".\"deptno\" > 20\n" + + "group by \"empid\", \"depts\".\"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[20], expr#3=[>($t1, $t2)], " + + "empid=[$t0], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs5() { + checkMaterialize( + "select \"depts\".\"deptno\", \"emps\".\"empid\" from \"depts\"\n" + + "join \"emps\" using (\"deptno\") where \"emps\".\"empid\" > 10\n" + + "group by \"depts\".\"deptno\", \"emps\".\"empid\"", + "select \"depts\".\"deptno\" from \"depts\"\n" + + "join \"emps\" using (\"deptno\") where \"emps\".\"empid\" > 15\n" + + "group by \"depts\".\"deptno\", \"emps\".\"empid\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0..1=[{inputs}], expr#2=[15], expr#3=[>($t1, $t2)], " + + "deptno=[$t0], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testJoinAggregateMaterializationNoAggregateFuncs6() { + checkMaterialize( + "select \"depts\".\"deptno\", \"emps\".\"empid\" from \"depts\"\n" + + "join \"emps\" using (\"deptno\") where \"emps\".\"empid\" > 10\n" + + "group by \"depts\".\"deptno\", \"emps\".\"empid\"", + "select \"depts\".\"deptno\" from \"depts\"\n" + + "join \"emps\" using (\"deptno\") where \"emps\".\"empid\" > 15\n" + + "group by \"depts\".\"deptno\"", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableAggregate(group=[{0}])\n" + + " EnumerableCalc(expr#0..1=[{inputs}], expr#2=[15], expr#3=[>($t1, $t2)], " + + "proj#0..1=[{exprs}], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + + @Test public void testJoinMaterialization4() { + checkMaterialize( + "select \"empid\" \"deptno\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\")", + "select \"empid\" \"deptno\" from \"emps\"\n" + + "join \"depts\" using (\"deptno\") where \"empid\" = 1", + JdbcTest.HR_MODEL, + CalciteAssert.checkResultContains( + "EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):INTEGER NOT NULL], expr#2=[1], " + + "expr#3=[=($t1, $t2)], deptno=[$t0], $condition=[$t3])\n" + + " EnumerableTableScan(table=[[hr, m0]])")); + } + @Test public void testSubQuery() { String q = "select \"empid\", \"deptno\", \"salary\" from \"emps\" e1\n" + "where \"empid\" = (\n"
