| /* |
| * 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.Contexts; |
| import org.apache.calcite.plan.RelOptRuleCall; |
| import org.apache.calcite.plan.RelOptRuleOperand; |
| import org.apache.calcite.plan.RelOptUtil; |
| import org.apache.calcite.plan.RelRule; |
| import org.apache.calcite.plan.Strong; |
| import org.apache.calcite.rel.RelNode; |
| 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.ProjectFactory; |
| import org.apache.calcite.rel.logical.LogicalJoin; |
| import org.apache.calcite.rel.logical.LogicalProject; |
| import org.apache.calcite.rel.type.RelDataType; |
| import org.apache.calcite.rel.type.RelDataTypeField; |
| import org.apache.calcite.rex.RexBuilder; |
| import org.apache.calcite.rex.RexLocalRef; |
| import org.apache.calcite.rex.RexNode; |
| import org.apache.calcite.rex.RexProgram; |
| import org.apache.calcite.rex.RexProgramBuilder; |
| import org.apache.calcite.sql.validate.SqlValidatorUtil; |
| import org.apache.calcite.tools.RelBuilder; |
| import org.apache.calcite.tools.RelBuilderFactory; |
| import org.apache.calcite.util.Pair; |
| |
| import org.checkerframework.checker.nullness.qual.Nullable; |
| import org.immutables.value.Value; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import static java.util.Objects.requireNonNull; |
| |
| /** |
| * Planner rule that matches a |
| * {@link org.apache.calcite.rel.core.Join} one of whose inputs is a |
| * {@link org.apache.calcite.rel.logical.LogicalProject}, and |
| * pulls the project up. |
| * |
| * <p>Projections are pulled up if the |
| * {@link org.apache.calcite.rel.logical.LogicalProject} doesn't originate from |
| * a null generating input in an outer join. |
| */ |
| @Value.Enclosing |
| public class JoinProjectTransposeRule |
| extends RelRule<JoinProjectTransposeRule.Config> |
| implements TransformationRule { |
| |
| /** Creates a JoinProjectTransposeRule. */ |
| protected JoinProjectTransposeRule(Config config) { |
| super(config); |
| } |
| |
| @Deprecated // to be removed before 2.0 |
| public JoinProjectTransposeRule(RelOptRuleOperand operand, |
| String description, boolean includeOuter, |
| RelBuilderFactory relBuilderFactory) { |
| this(Config.DEFAULT.withDescription(description) |
| .withRelBuilderFactory(relBuilderFactory) |
| .withOperandSupplier(b -> b.exactly(operand)) |
| .as(Config.class) |
| .withIncludeOuter(includeOuter)); |
| } |
| |
| @Deprecated // to be removed before 2.0 |
| public JoinProjectTransposeRule( |
| RelOptRuleOperand operand, |
| String description) { |
| this(Config.DEFAULT.withDescription(description) |
| .withOperandSupplier(b -> b.exactly(operand)) |
| .as(Config.class)); |
| } |
| |
| @Deprecated // to be removed before 2.0 |
| public JoinProjectTransposeRule(RelOptRuleOperand operand, |
| String description, ProjectFactory projectFactory) { |
| this(Config.DEFAULT.withDescription(description) |
| .withRelBuilderFactory(RelBuilder.proto(Contexts.of(projectFactory))) |
| .withOperandSupplier(b -> b.exactly(operand)) |
| .as(Config.class)); |
| } |
| |
| @Deprecated // to be removed before 2.0 |
| public JoinProjectTransposeRule(RelOptRuleOperand operand, |
| String description, boolean includeOuter, |
| ProjectFactory projectFactory) { |
| this(Config.DEFAULT.withDescription(description) |
| .withRelBuilderFactory(RelBuilder.proto(Contexts.of(projectFactory))) |
| .withOperandSupplier(b -> b.exactly(operand)) |
| .as(Config.class) |
| .withIncludeOuter(includeOuter)); |
| } |
| |
| //~ Methods ---------------------------------------------------------------- |
| |
| @Override public void onMatch(RelOptRuleCall call) { |
| final Join join = call.rel(0); |
| final JoinRelType joinType = join.getJoinType(); |
| |
| Project leftProject; |
| Project rightProject; |
| RelNode leftJoinChild; |
| RelNode rightJoinChild; |
| |
| // If 1) the rule works on outer joins, or |
| // 2) input's projection doesn't generate nulls |
| final boolean includeOuter = config.isIncludeOuter(); |
| if (hasLeftChild(call) |
| && (includeOuter || !joinType.generatesNullsOnLeft())) { |
| leftProject = call.rel(1); |
| leftJoinChild = getProjectChild(call, leftProject, true); |
| } else { |
| leftProject = null; |
| leftJoinChild = call.rel(1); |
| } |
| if (hasRightChild(call) |
| && (includeOuter || !joinType.generatesNullsOnRight())) { |
| rightProject = getRightChild(call); |
| rightJoinChild = getProjectChild(call, rightProject, false); |
| } else { |
| rightProject = null; |
| rightJoinChild = join.getRight(); |
| } |
| |
| // Skip projects containing over clause |
| if (leftProject != null && leftProject.containsOver()) { |
| leftProject = null; |
| leftJoinChild = join.getLeft(); |
| } |
| if (rightProject != null && rightProject.containsOver()) { |
| rightProject = null; |
| rightJoinChild = join.getRight(); |
| } |
| |
| if ((leftProject == null) && (rightProject == null)) { |
| return; |
| } |
| |
| if (includeOuter) { |
| if (leftProject != null && joinType.generatesNullsOnLeft() |
| && !Strong.allStrong(leftProject.getProjects())) { |
| return; |
| } |
| |
| if (rightProject != null && joinType.generatesNullsOnRight() |
| && !Strong.allStrong(rightProject.getProjects())) { |
| return; |
| } |
| } |
| |
| // Construct two RexPrograms and combine them. The bottom program |
| // is a join of the projection expressions from the left and/or |
| // right projects that feed into the join. The top program contains |
| // the join condition. |
| |
| // Create a row type representing a concatenation of the inputs |
| // underneath the projects that feed into the join. This is the input |
| // into the bottom RexProgram. Note that the join type is an inner |
| // join because the inputs haven't actually been joined yet. |
| final RelDataType joinChildrenRowType = |
| SqlValidatorUtil.deriveJoinRowType( |
| leftJoinChild.getRowType(), |
| rightJoinChild.getRowType(), |
| JoinRelType.INNER, |
| join.getCluster().getTypeFactory(), |
| null, |
| Collections.emptyList()); |
| |
| // Create projection expressions, combining the projection expressions |
| // from the projects that feed into the join. For the RHS projection |
| // expressions, shift them to the right by the number of fields on |
| // the LHS. If the join input was not a projection, simply create |
| // references to the inputs. |
| final int nProjExprs = join.getRowType().getFieldCount(); |
| final List<Pair<RexNode, String>> projects = new ArrayList<>(); |
| final RexBuilder rexBuilder = join.getCluster().getRexBuilder(); |
| |
| createProjectExprs( |
| leftProject, |
| leftJoinChild, |
| 0, |
| rexBuilder, |
| joinChildrenRowType.getFieldList(), |
| projects); |
| |
| final List<RelDataTypeField> leftFields = |
| leftJoinChild.getRowType().getFieldList(); |
| final int nFieldsLeft = leftFields.size(); |
| createProjectExprs( |
| rightProject, |
| rightJoinChild, |
| nFieldsLeft, |
| rexBuilder, |
| joinChildrenRowType.getFieldList(), |
| projects); |
| |
| final List<RelDataType> projTypes = new ArrayList<>(); |
| for (int i = 0; i < nProjExprs; i++) { |
| projTypes.add(projects.get(i).left.getType()); |
| } |
| RelDataType projRowType = |
| rexBuilder.getTypeFactory().createStructType( |
| projTypes, |
| Pair.right(projects)); |
| |
| // create the RexPrograms and merge them |
| final RexProgram bottomProgram = |
| RexProgram.create( |
| joinChildrenRowType, |
| Pair.left(projects), |
| null, |
| projRowType, |
| rexBuilder); |
| final RexProgramBuilder topProgramBuilder = |
| new RexProgramBuilder( |
| projRowType, |
| rexBuilder); |
| topProgramBuilder.addIdentity(); |
| topProgramBuilder.addCondition(join.getCondition()); |
| final RexProgram topProgram = topProgramBuilder.getProgram(); |
| final RexProgram mergedProgram = |
| RexProgramBuilder.mergePrograms( |
| topProgram, |
| bottomProgram, |
| rexBuilder); |
| |
| // expand out the join condition and construct a new LogicalJoin that |
| // directly references the join children without the intervening |
| // ProjectRels |
| final RexNode newCondition = |
| mergedProgram.expandLocalRef( |
| requireNonNull(mergedProgram.getCondition(), |
| () -> "mergedProgram.getCondition() for " + mergedProgram)); |
| final Join newJoin = |
| join.copy(join.getTraitSet(), newCondition, |
| leftJoinChild, rightJoinChild, join.getJoinType(), |
| join.isSemiJoinDone()); |
| |
| // expand out the new projection expressions; if the join is an |
| // outer join, modify the expressions to reference the join output |
| final List<RexNode> newProjExprs = new ArrayList<>(); |
| final List<RexLocalRef> projList = mergedProgram.getProjectList(); |
| final List<RelDataTypeField> newJoinFields = |
| newJoin.getRowType().getFieldList(); |
| final int nJoinFields = newJoinFields.size(); |
| int[] adjustments = new int[nJoinFields]; |
| for (int i = 0; i < nProjExprs; i++) { |
| RexNode newExpr = mergedProgram.expandLocalRef(projList.get(i)); |
| if (joinType.isOuterJoin()) { |
| newExpr = |
| newExpr.accept( |
| new RelOptUtil.RexInputConverter( |
| rexBuilder, |
| joinChildrenRowType.getFieldList(), |
| newJoinFields, |
| adjustments)); |
| } |
| newProjExprs.add(newExpr); |
| } |
| |
| // finally, create the projection on top of the join |
| final RelBuilder relBuilder = call.builder(); |
| relBuilder.push(newJoin); |
| relBuilder.project(newProjExprs, join.getRowType().getFieldNames()); |
| // if the join was outer, we might need a cast after the |
| // projection to fix differences wrt nullability of fields |
| if (joinType.isOuterJoin()) { |
| relBuilder.convert(join.getRowType(), false); |
| } |
| |
| call.transformTo(relBuilder.build()); |
| } |
| |
| /** Returns whether the rule was invoked with a left project child. */ |
| protected boolean hasLeftChild(RelOptRuleCall call) { |
| return call.rel(1) instanceof Project; |
| } |
| |
| /** Returns whether the rule was invoked with 2 children. */ |
| protected boolean hasRightChild(RelOptRuleCall call) { |
| return call.rels.length == 3; |
| } |
| |
| /** Returns the Project corresponding to the right child. */ |
| protected Project getRightChild(RelOptRuleCall call) { |
| return call.rel(2); |
| } |
| |
| /** |
| * Returns the child of the project that will be used as input into the new |
| * LogicalJoin once the projects are pulled above the LogicalJoin. |
| * |
| * @param call RelOptRuleCall |
| * @param project project RelNode |
| * @param leftChild true if the project corresponds to the left projection |
| * @return child of the project that will be used as input into the new |
| * LogicalJoin once the projects are pulled above the LogicalJoin |
| */ |
| protected RelNode getProjectChild( |
| RelOptRuleCall call, |
| Project project, |
| boolean leftChild) { |
| return project.getInput(); |
| } |
| |
| /** |
| * Creates projection expressions corresponding to one of the inputs into |
| * the join. |
| * |
| * @param project the projection input into the join (if it exists) |
| * @param joinChild the child of the projection input (if there is a |
| * projection); otherwise, this is the join input |
| * @param adjustmentAmount the amount the expressions need to be shifted by |
| * @param rexBuilder rex builder |
| * @param joinChildrenFields concatenation of the fields from the left and |
| * right join inputs (once the projections have been |
| * removed) |
| * @param projects Projection expressions & names to be created |
| */ |
| protected void createProjectExprs( |
| @Nullable Project project, |
| RelNode joinChild, |
| int adjustmentAmount, |
| RexBuilder rexBuilder, |
| List<RelDataTypeField> joinChildrenFields, |
| List<Pair<RexNode, String>> projects) { |
| List<RelDataTypeField> childFields = |
| joinChild.getRowType().getFieldList(); |
| if (project != null) { |
| List<Pair<RexNode, String>> namedProjects = |
| project.getNamedProjects(); |
| int nChildFields = childFields.size(); |
| int[] adjustments = new int[nChildFields]; |
| for (int i = 0; i < nChildFields; i++) { |
| adjustments[i] = adjustmentAmount; |
| } |
| for (Pair<RexNode, String> pair : namedProjects) { |
| RexNode e = pair.left; |
| if (adjustmentAmount != 0) { |
| // shift the references by the adjustment amount |
| e = e.accept( |
| new RelOptUtil.RexInputConverter( |
| rexBuilder, |
| childFields, |
| joinChildrenFields, |
| adjustments)); |
| } |
| projects.add(Pair.of(e, pair.right)); |
| } |
| } else { |
| // no projection; just create references to the inputs |
| for (int i = 0; i < childFields.size(); i++) { |
| final RelDataTypeField field = childFields.get(i); |
| projects.add( |
| Pair.of( |
| rexBuilder.makeInputRef(field.getType(), |
| i + adjustmentAmount), |
| field.getName())); |
| } |
| } |
| } |
| |
| /** Rule configuration. */ |
| @Value.Immutable |
| public interface Config extends RelRule.Config { |
| Config DEFAULT = ImmutableJoinProjectTransposeRule.Config.of() |
| .withOperandSupplier(b0 -> |
| b0.operand(LogicalJoin.class).inputs( |
| b1 -> b1.operand(LogicalProject.class).anyInputs(), |
| b2 -> b2.operand(LogicalProject.class).anyInputs())) |
| .withDescription("JoinProjectTransposeRule(Project-Project)"); |
| |
| Config LEFT = DEFAULT |
| .withOperandSupplier(b0 -> |
| b0.operand(LogicalJoin.class).inputs( |
| b1 -> b1.operand(LogicalProject.class).anyInputs())) |
| .withDescription("JoinProjectTransposeRule(Project-Other)") |
| .as(Config.class); |
| |
| Config RIGHT = DEFAULT |
| .withOperandSupplier(b0 -> |
| b0.operand(LogicalJoin.class).inputs( |
| b1 -> b1.operand(RelNode.class).anyInputs(), |
| b2 -> b2.operand(LogicalProject.class).anyInputs())) |
| .withDescription("JoinProjectTransposeRule(Other-Project)") |
| .as(Config.class); |
| |
| Config OUTER = DEFAULT |
| .withDescription( |
| "Join(IncludingOuter)ProjectTransposeRule(Project-Project)") |
| .as(Config.class) |
| .withIncludeOuter(true); |
| |
| Config LEFT_OUTER = LEFT |
| .withDescription( |
| "Join(IncludingOuter)ProjectTransposeRule(Project-Other)") |
| .as(Config.class) |
| .withIncludeOuter(true); |
| |
| Config RIGHT_OUTER = RIGHT |
| .withDescription( |
| "Join(IncludingOuter)ProjectTransposeRule(Other-Project)") |
| .as(Config.class) |
| .withIncludeOuter(true); |
| |
| @Override default JoinProjectTransposeRule toRule() { |
| return new JoinProjectTransposeRule(this); |
| } |
| |
| /** Whether to include outer joins, default false. */ |
| @Value.Default default boolean isIncludeOuter() { |
| return false; |
| } |
| |
| /** Sets {@link #isIncludeOuter()}. */ |
| Config withIncludeOuter(boolean includeOuter); |
| } |
| } |