All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.druid.sql.calcite.rule.DruidJoinRule Maven / Gradle / Ivy

There is a newer version: 31.0.0
Show newest version
/*
 * 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.druid.sql.calcite.rule;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelNode;
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.type.RelDataType;
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.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexSlot;
import org.apache.calcite.rex.RexUtil;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.error.DruidException;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.query.LookupDataSource;
import org.apache.druid.sql.calcite.planner.PlannerContext;
import org.apache.druid.sql.calcite.rel.DruidJoinQueryRel;
import org.apache.druid.sql.calcite.rel.DruidQueryRel;
import org.apache.druid.sql.calcite.rel.DruidRel;
import org.apache.druid.sql.calcite.rel.PartialDruidQuery;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;

public class DruidJoinRule extends RelOptRule
{

  private final boolean enableLeftScanDirect;
  private final PlannerContext plannerContext;

  private DruidJoinRule(final PlannerContext plannerContext)
  {
    super(
        operand(
            Join.class,
            operand(DruidRel.class, any()),
            operand(DruidRel.class, any())
        )
    );
    this.enableLeftScanDirect = plannerContext.queryContext().getEnableJoinLeftScanDirect();
    this.plannerContext = plannerContext;
  }

  public static DruidJoinRule instance(PlannerContext plannerContext)
  {
    return new DruidJoinRule(plannerContext);
  }

  @Override
  public boolean matches(RelOptRuleCall call)
  {
    final Join join = call.rel(0);
    final DruidRel left = call.rel(1);
    final DruidRel right = call.rel(2);

    // 1) Can handle the join condition as a native join.
    // 2) Left has a PartialDruidQuery (i.e., is a real query, not top-level UNION ALL).
    // 3) Right has a PartialDruidQuery (i.e., is a real query, not top-level UNION ALL).
    return canHandleCondition(
        join.getCondition(),
        join.getLeft().getRowType(),
        right,
        join.getJoinType(),
        join.getSystemFieldList(),
        join.getCluster().getRexBuilder()
    ) && left.getPartialDruidQuery() != null && right.getPartialDruidQuery() != null;
  }

  @Override
  public void onMatch(RelOptRuleCall call)
  {
    final Join join = call.rel(0);
    final DruidRel left = call.rel(1);
    final DruidRel right = call.rel(2);

    final RexBuilder rexBuilder = join.getCluster().getRexBuilder();

    final DruidRel newLeft;
    final DruidRel newRight;
    final Filter leftFilter;
    final List newProjectExprs = new ArrayList<>();

    // Can't be final, because we're going to reassign it up to a couple of times.
    ConditionAnalysis conditionAnalysis = analyzeCondition(
        join.getCondition(),
        join.getLeft().getRowType(),
        rexBuilder
    );
    plannerContext.setPlanningError(conditionAnalysis.errorStr);
    final boolean isLeftDirectAccessPossible = enableLeftScanDirect && (left instanceof DruidQueryRel);

    if (!plannerContext.getJoinAlgorithm().requiresSubquery()
        && left.getPartialDruidQuery().stage() == PartialDruidQuery.Stage.SELECT_PROJECT
        && (isLeftDirectAccessPossible || left.getPartialDruidQuery().getWhereFilter() == null)) {
      // Swap the left-side projection above the join, so the left side is a simple scan or mapping. This helps us
      // avoid subqueries.
      final RelNode leftScan = left.getPartialDruidQuery().getScan();
      final Project leftProject = left.getPartialDruidQuery().getSelectProject();
      leftFilter = left.getPartialDruidQuery().getWhereFilter();

      // Left-side projection expressions rewritten to be on top of the join.
      newProjectExprs.addAll(leftProject.getProjects());
      newLeft = left.withPartialQuery(PartialDruidQuery.create(leftScan));
      conditionAnalysis = conditionAnalysis.pushThroughLeftProject(leftProject);
    } else {
      // Leave left as-is. Write input refs that do nothing.
      for (int i = 0; i < left.getRowType().getFieldCount(); i++) {
        newProjectExprs.add(rexBuilder.makeInputRef(join.getRowType().getFieldList().get(i).getType(), i));
      }

      newLeft = left;
      leftFilter = null;
    }

    if (!plannerContext.getJoinAlgorithm().requiresSubquery()
        && right.getPartialDruidQuery().stage() == PartialDruidQuery.Stage.SELECT_PROJECT
        && right.getPartialDruidQuery().getWhereFilter() == null
        && !right.getPartialDruidQuery().getSelectProject().isMapping()
        && conditionAnalysis.onlyUsesMappingsFromRightProject(right.getPartialDruidQuery().getSelectProject())) {
      // Swap the right-side projection above the join, so the right side is a simple scan or mapping. This helps us
      // avoid subqueries.
      final RelNode rightScan = right.getPartialDruidQuery().getScan();
      final Project rightProject = right.getPartialDruidQuery().getSelectProject();

      // Right-side projection expressions rewritten to be on top of the join.
      
      for (final RexNode rexNode : RexUtil.shift(rightProject.getProjects(), newLeft.getRowType().getFieldCount())) {
        if (join.getJoinType().generatesNullsOnRight()) {
          newProjectExprs.add(makeNullableIfLiteral(rexNode, rexBuilder));
        } else {
          newProjectExprs.add(rexNode);
        }
      }

      newRight = right.withPartialQuery(PartialDruidQuery.create(rightScan));
      conditionAnalysis = conditionAnalysis.pushThroughRightProject(rightProject);
    } else {
      // Leave right as-is. Write input refs that do nothing.
      for (int i = 0; i < right.getRowType().getFieldCount(); i++) {
        newProjectExprs.add(
            rexBuilder.makeInputRef(
                join.getRowType().getFieldList().get(left.getRowType().getFieldCount() + i).getType(),
                newLeft.getRowType().getFieldCount() + i
            )
        );
      }

      newRight = right;
    }

    // Druid join written on top of the new left and right sides.
    final DruidJoinQueryRel druidJoin = DruidJoinQueryRel.create(
        join.copy(
            join.getTraitSet(),
            conditionAnalysis.getConditionWithUnsupportedSubConditionsIgnored(rexBuilder),
            newLeft,
            newRight,
            join.getJoinType(),
            join.isSemiJoinDone()
        ),
        leftFilter,
        left.getPlannerContext()
    );

    RelBuilder relBuilder =
        call.builder()
            .push(druidJoin)
            .project(
                RexUtil.fixUp(
                    rexBuilder,
                    newProjectExprs,
                    RelOptUtil.getFieldTypeList(druidJoin.getRowType())
                )
            );

    // Build a post-join filter with whatever join sub-conditions were not supported.
    RexNode postJoinFilter = RexUtil.composeConjunction(rexBuilder, conditionAnalysis.getUnsupportedOnSubConditions(), true);
    if (postJoinFilter != null) {
      relBuilder = relBuilder.filter(postJoinFilter);
    }
    relBuilder.convert(join.getRowType(), false);
    call.transformTo(relBuilder.build());
  }

  private static RexNode makeNullableIfLiteral(final RexNode rexNode, final RexBuilder rexBuilder)
  {
    if (rexNode.isA(SqlKind.LITERAL)) {
      return rexBuilder.makeLiteral(
          RexLiteral.value(rexNode),
          rexBuilder.getTypeFactory().createTypeWithNullability(rexNode.getType(), true),
          true
      );
    } else {
      return rexNode;
    }
  }

  /**
   * Returns whether we can handle the join condition. In case, some conditions in an AND expression are not supported,
   * they are extracted into a post-join filter instead.
   */
  @VisibleForTesting
  public boolean canHandleCondition(
      final RexNode condition,
      final RelDataType leftRowType,
      DruidRel right,
      JoinRelType joinType,
      List systemFieldList,
      final RexBuilder rexBuilder
  )
  {
    ConditionAnalysis conditionAnalysis = analyzeCondition(condition, leftRowType, rexBuilder);
    plannerContext.setPlanningError(conditionAnalysis.errorStr);
    // if the right side requires a subquery, then even lookup will be transformed to a QueryDataSource
    // thereby allowing join conditions on both k and v columns of the lookup
    if (right != null
        && !DruidJoinQueryRel.computeRightRequiresSubquery(plannerContext, DruidJoinQueryRel.getSomeDruidChild(right))
        && right instanceof DruidQueryRel) {
      DruidQueryRel druidQueryRel = (DruidQueryRel) right;
      if (druidQueryRel.getDruidTable().getDataSource() instanceof LookupDataSource) {
        long distinctRightColumns = conditionAnalysis.rightColumns.stream().map(RexSlot::getIndex).distinct().count();
        if (distinctRightColumns > 1) {
          // it means that the join's right side is lookup and the join condition contains both key and value columns of lookup.
          // currently, the lookup datasource in the native engine doesn't support using value column in the join condition.
          plannerContext.setPlanningError(
              "SQL is resulting in a join involving lookup where value column is used in the condition.");
          return false;
        }
      }
    }

    if (joinType != JoinRelType.INNER || !systemFieldList.isEmpty() || NullHandling.replaceWithDefault()) {
      // I am not sure in what case, the list of system fields will be not empty. I have just picked up this logic
      // directly from https://github.com/apache/calcite/blob/calcite-1.35.0/core/src/main/java/org/apache/calcite/rel/rules/AbstractJoinExtractFilterRule.java#L58

      // Also to avoid results changes for existing queries in non-null handling mode, we don't handle unsupported
      // conditions. Otherwise, some left/right joins with a condition that doesn't allow nulls on join input will
      // be converted to inner joins. See Test CalciteJoinQueryTest#testFilterAndGroupByLookupUsingJoinOperatorBackwards
      // for an example.
      return conditionAnalysis.getUnsupportedOnSubConditions().isEmpty();
    }

    return true;
  }

  public static class ConditionAnalysis
  {
    /**
     * Number of fields on the left-hand side. Useful for identifying if a particular field is from on the left
     * or right side of a join.
     */
    private final int numLeftFields;

    /**
     * Each equality subcondition is an equality of the form f(LeftRel) = g(RightRel).
     */
    private final List equalitySubConditions;

    /**
     * Each literal subcondition is... a literal.
     */
    private final List literalSubConditions;

    /**
     * Sub-conditions in join clause that cannot be handled by the DruidJoinRule.
     */
    private final List unsupportedOnSubConditions;

    private final Set rightColumns;

    public final String errorStr;

    ConditionAnalysis(
        int numLeftFields,
        List equalitySubConditions,
        List literalSubConditions,
        List unsupportedOnSubConditions,
        Set rightColumns,
        String errorStr
    )
    {
      this.numLeftFields = numLeftFields;
      this.equalitySubConditions = equalitySubConditions;
      this.literalSubConditions = literalSubConditions;
      this.unsupportedOnSubConditions = unsupportedOnSubConditions;
      this.rightColumns = rightColumns;
      this.errorStr = errorStr;
    }

    public ConditionAnalysis pushThroughLeftProject(final Project leftProject)
    {
      // Pushing through the project will shift right-hand field references by this amount.
      final int rhsShift =
          leftProject.getInput().getRowType().getFieldCount() - leftProject.getRowType().getFieldCount();

      // We leave unsupportedSubConditions un-touched as they are evaluated above join anyway.
      return new ConditionAnalysis(
          leftProject.getInput().getRowType().getFieldCount(),
          equalitySubConditions
              .stream()
              .map(
                  equality -> new RexEquality(
                      RelOptUtil.pushPastProject(equality.left, leftProject),
                      (RexInputRef) RexUtil.shift(equality.right, rhsShift),
                      equality.kind
                  )
              )
              .collect(Collectors.toList()),
          literalSubConditions,
          unsupportedOnSubConditions,
          rightColumns,
          null
      );
    }

    public ConditionAnalysis pushThroughRightProject(final Project rightProject)
    {
      Preconditions.checkArgument(onlyUsesMappingsFromRightProject(rightProject), "Cannot push through");

      // We leave unsupportedSubConditions un-touched as they are evaluated above join anyway.
      return new ConditionAnalysis(
          numLeftFields,
          equalitySubConditions
              .stream()
              .map(
                  equality -> new RexEquality(
                      equality.left,
                      (RexInputRef) RexUtil.shift(
                          RelOptUtil.pushPastProject(
                              RexUtil.shift(equality.right, -numLeftFields),
                              rightProject
                          ),
                          numLeftFields
                      ),
                      equality.kind
                  )
              )
              .collect(Collectors.toList()),
          literalSubConditions,
          unsupportedOnSubConditions,
          rightColumns,
          null
      );
    }

    public boolean onlyUsesMappingsFromRightProject(final Project rightProject)
    {
      for (final RexEquality equality : equalitySubConditions) {
        final int rightIndex = equality.right.getIndex() - numLeftFields;

        if (!rightProject.getProjects().get(rightIndex).isA(SqlKind.INPUT_REF)) {
          return false;
        }
      }

      return true;
    }

    public RexNode getConditionWithUnsupportedSubConditionsIgnored(final RexBuilder rexBuilder)
    {
      return RexUtil.composeConjunction(
          rexBuilder,
          Iterables.concat(
              literalSubConditions,
              equalitySubConditions
                  .stream()
                  .map(equality -> equality.makeCall(rexBuilder))
                  .collect(Collectors.toList())
          ),
          false
      );
    }

    public List getUnsupportedOnSubConditions()
    {
      return unsupportedOnSubConditions;
    }

    @Override
    public String toString()
    {
      return "ConditionAnalysis{" +
             "numLeftFields=" + numLeftFields +
             ", equalitySubConditions=" + equalitySubConditions +
             ", literalSubConditions=" + literalSubConditions +
             ", unsupportedSubConditions=" + unsupportedOnSubConditions +
             ", rightColumns=" + rightColumns +
             '}';
    }
  }

  /**
   * If this condition is an AND of some combination of
   * (1) literals;
   * (2) equality conditions of the form
   * (3) unsupported conditions
   * 

* Returns empty if the join cannot be supported at all. It can return non-empty with some unsupported conditions * that can be extracted into post join filter. * {@code f(LeftRel) = RightColumn}, then return a {@link ConditionAnalysis}. */ public static ConditionAnalysis analyzeCondition( final RexNode condition, final RelDataType leftRowType, final RexBuilder rexBuilder ) { final List subConditions = decomposeAnd(condition); final List equalitySubConditions = new ArrayList<>(); final List literalSubConditions = new ArrayList<>(); final List unSupportedSubConditions = new ArrayList<>(); final Set rightColumns = new HashSet<>(); final int numLeftFields = leftRowType.getFieldCount(); final List errors = new ArrayList(); for (RexNode subCondition : subConditions) { if (RexUtil.isLiteral(subCondition, true)) { if (subCondition.isA(SqlKind.CAST)) { // This is CAST(literal) which is always OK. // We know that this is CAST(literal) as it passed the check from RexUtil.isLiteral RexCall call = (RexCall) subCondition; // We have to verify the types of the cast here, because if the underlying literal and the cast output type // are different, then skipping the cast might change the meaning of the subcondition. if (call.getType().getSqlTypeName().equals(call.getOperands().get(0).getType().getSqlTypeName())) { // If the types are the same, unwrap the cast and use the underlying literal. literalSubConditions.add((RexLiteral) call.getOperands().get(0)); } else { // If the types are not the same, add to unsupported conditions. unSupportedSubConditions.add(subCondition); continue; } } else { // Literals are always OK. literalSubConditions.add((RexLiteral) subCondition); } continue; } RexNode firstOperand; RexNode secondOperand; SqlKind comparisonKind; if (subCondition.isA(SqlKind.INPUT_REF)) { firstOperand = rexBuilder.makeLiteral(true); secondOperand = subCondition; comparisonKind = SqlKind.EQUALS; if (!SqlTypeName.BOOLEAN_TYPES.contains(secondOperand.getType().getSqlTypeName())) { errors.add( StringUtils.format( "SQL requires a join with '%s' condition where the column is of the type %s, that is not supported", subCondition.getKind(), secondOperand.getType().getSqlTypeName() ) ); unSupportedSubConditions.add(subCondition); continue; } } else if (subCondition.isA(SqlKind.EQUALS) || subCondition.isA(SqlKind.IS_NOT_DISTINCT_FROM)) { final List operands = ((RexCall) subCondition).getOperands(); Preconditions.checkState(operands.size() == 2, "Expected 2 operands, got[%s]", operands.size()); firstOperand = operands.get(0); secondOperand = operands.get(1); comparisonKind = subCondition.getKind(); } else { // If it's not EQUALS or a BOOLEAN input ref, it's not supported. errors.add( StringUtils.format( "SQL requires a join with '%s' condition that is not supported.", subCondition.getKind() ) ); unSupportedSubConditions.add(subCondition); continue; } if (isLeftExpression(firstOperand, numLeftFields) && isRightInputRef(secondOperand, numLeftFields)) { equalitySubConditions.add(new RexEquality(firstOperand, (RexInputRef) secondOperand, comparisonKind)); rightColumns.add((RexInputRef) secondOperand); } else if (isRightInputRef(firstOperand, numLeftFields) && isLeftExpression(secondOperand, numLeftFields)) { equalitySubConditions.add(new RexEquality(secondOperand, (RexInputRef) firstOperand, subCondition.getKind())); rightColumns.add((RexInputRef) firstOperand); } else { // Cannot handle this condition. errors.add( StringUtils.format( "SQL is resulting in a join that has unsupported operand types." ) ); unSupportedSubConditions.add(subCondition); } } final String errorStr; if (errors.size() > 0) { errorStr = Joiner.on('\n').join(errors); } else { errorStr = null; } return new ConditionAnalysis( numLeftFields, equalitySubConditions, literalSubConditions, unSupportedSubConditions, rightColumns, errorStr ); } @VisibleForTesting static List decomposeAnd(final RexNode condition) { final List retVal = new ArrayList<>(); final Stack stack = new Stack<>(); stack.push(condition); while (!stack.empty()) { final RexNode current = stack.pop(); if (current.isA(SqlKind.AND)) { final List operands = ((RexCall) current).getOperands(); // Add right-to-left, so when we unwind the stack, the operands are in the original order. for (int i = operands.size() - 1; i >= 0; i--) { stack.push(operands.get(i)); } } else { retVal.add(current); } } return retVal; } private static boolean isLeftExpression(final RexNode rexNode, final int numLeftFields) { return ImmutableBitSet.range(numLeftFields).contains(RelOptUtil.InputFinder.bits(rexNode)); } private static boolean isRightInputRef(final RexNode rexNode, final int numLeftFields) { return rexNode.isA(SqlKind.INPUT_REF) && ((RexInputRef) rexNode).getIndex() >= numLeftFields; } /** * Like {@link org.apache.druid.segment.join.Equality} but uses {@link RexNode} instead of * {@link org.apache.druid.math.expr.Expr}. */ static class RexEquality { private final RexNode left; private final RexInputRef right; private final SqlKind kind; public RexEquality(RexNode left, RexInputRef right, SqlKind kind) { this.left = left; this.right = right; this.kind = kind; } public RexNode makeCall(final RexBuilder builder) { final SqlOperator operator; if (kind == SqlKind.EQUALS) { operator = SqlStdOperatorTable.EQUALS; } else if (kind == SqlKind.IS_NOT_DISTINCT_FROM) { operator = SqlStdOperatorTable.IS_NOT_DISTINCT_FROM; } else { throw DruidException.defensive("Unexpected operator kind[%s]", kind); } return builder.makeCall(operator, left, right); } @Override public String toString() { return "RexEquality{" + "left=" + left + ", right=" + right + ", kind=" + kind + '}'; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy