/** * 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.tajo.plan.rewrite.rules; import org.apache.tajo.exception.TajoException; import org.apache.tajo.plan.LogicalPlan; import org.apache.tajo.plan.expr.*; import org.apache.tajo.plan.logical.JoinNode; import org.apache.tajo.plan.logical.LogicalNode; import org.apache.tajo.plan.logical.NodeType; import org.apache.tajo.plan.logical.SelectionNode; import org.apache.tajo.plan.rewrite.LogicalPlanRewriteRule; import org.apache.tajo.plan.rewrite.LogicalPlanRewriteRuleContext; import org.apache.tajo.plan.visitor.BasicLogicalPlanVisitor; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.Stack; /** * Condition reduce rule reduces the query predicate based on distributivity. * For example, the query * * SELECT * * FROM t * WHERE (t.a = 1 OR t.b = 10) AND (t.a = 1 OR t.c = 100) * * is converted into * * SELECT * * FROM t * WHERE t.a = 1 OR (t.b = 10 AND t.c = 100). * */ public class CommonConditionReduceRule implements LogicalPlanRewriteRule { private Rewriter rewriter; @Override public String getName() { return "CommonConditionReduceRule"; } @Override public boolean isEligible(LogicalPlanRewriteRuleContext context) { for (LogicalPlan.QueryBlock block : context.getPlan().getQueryBlocks()) { if (block.hasNode(NodeType.SELECTION) || block.hasNode(NodeType.JOIN)) { rewriter = new Rewriter(context.getPlan()); return true; } } return false; } @Override public LogicalPlan rewrite(LogicalPlanRewriteRuleContext context) throws TajoException { rewriter.visit(null, context.getPlan(), context.getPlan().getRootBlock()); return context.getPlan(); } /** * Rewriter simply triggers rewriting evals while visiting logical nodes. */ private final static class Rewriter extends BasicLogicalPlanVisitor<Object, LogicalNode> { EvalRewriter evalRewriter; public Rewriter(LogicalPlan plan) { evalRewriter = new EvalRewriter(plan); } @Override public LogicalNode visitFilter(Object context, LogicalPlan plan, LogicalPlan.QueryBlock block, SelectionNode selNode, Stack<LogicalNode> stack) throws TajoException { selNode.setQual(evalRewriter.visit(null, selNode.getQual(), new Stack<>())); return null; } @Override public LogicalNode visitJoin(Object context, LogicalPlan plan, LogicalPlan.QueryBlock block, JoinNode joinNode, Stack<LogicalNode> stack) throws TajoException { if (joinNode.hasJoinQual()) { joinNode.setJoinQual(evalRewriter.visit(null, joinNode.getJoinQual(), new Stack<>())); } return null; } } /** * EvalRewriter is responsible for rewriting evals based on distributivity. */ private static class EvalRewriter extends SimpleEvalNodeVisitor<Object> { final LogicalPlan plan; public EvalRewriter(LogicalPlan plan) { this.plan = plan; } @Override protected EvalNode visitUnaryEval(Object context, UnaryEval unaryEval, Stack<EvalNode> stack) { stack.push(unaryEval); EvalNode child = unaryEval.getChild(); visit(context, child, stack); if (child.getType() == EvalType.AND || child.getType() == EvalType.OR) { unaryEval.setChild(rewrite((BinaryEval) child)); } stack.pop(); return unaryEval; } @Override protected EvalNode visitBinaryEval(Object context, Stack<EvalNode> stack, BinaryEval binaryEval) { stack.push(binaryEval); EvalNode child = binaryEval.getLeftExpr(); visit(context, child, stack); if (child.getType() == EvalType.AND || child.getType() == EvalType.OR) { binaryEval.setLeftExpr(rewrite((BinaryEval) child)); } child = binaryEval.getRightExpr(); visit(context, child, stack); if (child.getType() == EvalType.AND || child.getType() == EvalType.OR) { binaryEval.setRightExpr(rewrite((BinaryEval) child)); } EvalNode result = rewrite(binaryEval); stack.pop(); return result; } @Override protected EvalNode visitDefaultFunctionEval(Object context, Stack<EvalNode> stack, FunctionEval functionEval) { stack.push(functionEval); if (functionEval.getArgs() != null) { EvalNode [] args = functionEval.getArgs(); for (int i = 0; i < args.length; i++) { visit(context, args[i], stack); if (args[i].getType() == EvalType.AND || args[i].getType() == EvalType.OR) { functionEval.setArg(i, rewrite((BinaryEval) args[i])); } } } stack.pop(); return functionEval; } private EvalNode rewrite(BinaryEval evalNode) { // Example qual: ( a OR b ) AND ( a OR c ) EvalType outerType = evalNode.getType(); // type of the outer operation. ex) AND EvalNode finalQual = evalNode; if ((evalNode.getLeftExpr().getType() == EvalType.AND || evalNode.getLeftExpr().getType() == EvalType.OR) && evalNode.getLeftExpr().getType() == evalNode.getRightExpr().getType()) { EvalNode leftChild = evalNode.getLeftExpr(); EvalNode rightChild = evalNode.getRightExpr(); EvalType innerType = leftChild.getType(); // Find common quals from the left and right children. Set<EvalNode> commonQuals = new HashSet<>(); Set<EvalNode> leftChildSplits = innerType == EvalType.AND ? new HashSet<>(Arrays.asList(AlgebraicUtil.toConjunctiveNormalFormArray(leftChild))) : new HashSet<>(Arrays.asList(AlgebraicUtil.toDisjunctiveNormalFormArray(leftChild))); Set<EvalNode> rightChildSplits = innerType == EvalType.AND ? new HashSet<>(Arrays.asList(AlgebraicUtil.toConjunctiveNormalFormArray(rightChild))) : new HashSet<>(Arrays.asList(AlgebraicUtil.toDisjunctiveNormalFormArray(rightChild))); for (EvalNode eachLeftChildSplit : leftChildSplits) { if (rightChildSplits.contains(eachLeftChildSplit)) { commonQuals.add(eachLeftChildSplit); } } if (leftChildSplits.size() == rightChildSplits.size() && commonQuals.size() == leftChildSplits.size()) { // Ex) ( a OR b ) AND ( a OR b ) // Current binary eval has the same left and right children, so it is useless. // Connect the parent of the current eval and one of the children directly. finalQual = leftChild; } else if (commonQuals.size() == leftChildSplits.size()) { // Ex) ( a OR b ) AND ( a OR b OR c ) finalQual = rightChild; } else if (commonQuals.size() == rightChildSplits.size()) { // Ex) ( a OR b OR c ) AND ( a OR b ) finalQual = leftChild; } else if (commonQuals.size() > 0) { // Common quals are found. // ( a OR b ) AND ( a OR c ) -> a OR (b AND c) // Find non-common quals. leftChildSplits.removeAll(commonQuals); rightChildSplits.removeAll(commonQuals); // Recreate both children using non-common quals. EvalNode commonQual; if (innerType == EvalType.AND) { leftChild = AlgebraicUtil.createSingletonExprFromCNF(leftChildSplits); rightChild = AlgebraicUtil.createSingletonExprFromCNF(rightChildSplits); commonQual = AlgebraicUtil.createSingletonExprFromCNF(commonQuals); } else { leftChild = AlgebraicUtil.createSingletonExprFromDNF(leftChildSplits); rightChild = AlgebraicUtil.createSingletonExprFromDNF(rightChildSplits); commonQual = AlgebraicUtil.createSingletonExprFromDNF(commonQuals); } finalQual = new BinaryEval(innerType, commonQual, new BinaryEval(outerType, leftChild, rightChild)); } } else if (evalNode.getLeftExpr().equals(evalNode.getRightExpr())) { finalQual = evalNode.getLeftExpr(); } // Just compare that finalQual and evalNode is the same instance. if (finalQual != evalNode) { plan.addHistory("Common condition is reduced."); } return finalQual; } } }