blob: c7460e70162748a2063f28cdab8ae94612b87103 [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.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 &amp; 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);
}
}