/* * JBoss, Home of Professional Open Source. * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA. */ package org.teiid.query.optimizer.relational.rules; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.teiid.api.exception.query.QueryMetadataException; import org.teiid.api.exception.query.QueryPlannerException; import org.teiid.core.TeiidComponentException; import org.teiid.core.util.Assertion; import org.teiid.query.analysis.AnalysisRecord; import org.teiid.query.metadata.QueryMetadataInterface; import org.teiid.query.optimizer.capabilities.CapabilitiesFinder; import org.teiid.query.optimizer.relational.OptimizerRule; import org.teiid.query.optimizer.relational.RuleStack; import org.teiid.query.optimizer.relational.plantree.NodeConstants; import org.teiid.query.optimizer.relational.plantree.NodeConstants.Info; import org.teiid.query.optimizer.relational.plantree.NodeEditor; import org.teiid.query.optimizer.relational.plantree.NodeFactory; import org.teiid.query.optimizer.relational.plantree.PlanNode; import org.teiid.query.sql.lang.JoinType; import org.teiid.query.sql.lang.OrderBy; import org.teiid.query.sql.lang.OrderByItem; import org.teiid.query.sql.lang.SetQuery; import org.teiid.query.sql.symbol.AliasSymbol; import org.teiid.query.sql.symbol.ElementSymbol; import org.teiid.query.sql.symbol.Expression; import org.teiid.query.sql.symbol.GroupSymbol; import org.teiid.query.sql.symbol.Symbol; import org.teiid.query.sql.util.SymbolMap; import org.teiid.query.util.CommandContext; /** * Will attempt to raise null nodes to their highest points */ public final class RuleRaiseNull implements OptimizerRule { public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata, CapabilitiesFinder capFinder, RuleStack rules, AnalysisRecord analysisRecord, CommandContext context) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { List<PlanNode> nodes = NodeEditor.findAllNodes(plan, NodeConstants.Types.NULL); //create a new list to iterate over since the original will be modified for (PlanNode nullNode : new LinkedList<PlanNode>(nodes)) { while (nullNode.getParent() != null && nodes.contains(nullNode)) { // Attempt to raise the node PlanNode newRoot = raiseNullNode(plan, nodes, nullNode, metadata, capFinder); if(newRoot != null) { plan = newRoot; } else { break; } } if (nullNode.getParent() == null) { nodes.remove(nullNode); } } return plan; } /** * @param nullNode * @param metadata * @param capFinder * @return null if the raising should not continue, else the newRoot */ PlanNode raiseNullNode(PlanNode rootNode, List<PlanNode> nodes, PlanNode nullNode, QueryMetadataInterface metadata, CapabilitiesFinder capFinder) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { PlanNode parentNode = nullNode.getParent(); switch(parentNode.getType()) { case NodeConstants.Types.JOIN: { JoinType jt = (JoinType)parentNode.getProperty(NodeConstants.Info.JOIN_TYPE); if (jt == JoinType.JOIN_CROSS || jt == JoinType.JOIN_INNER) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } //for outer joins if the null node is on the outer side, then the join itself is null //if the null node is on the inner side, then the join can be removed but the null values //coming from the inner side will need to be placed into the frame if (jt == JoinType.JOIN_LEFT_OUTER) { if (nullNode == parentNode.getFirstChild()) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } raiseNullThroughJoin(metadata, parentNode, parentNode.getLastChild()); return null; } if (jt == JoinType.JOIN_RIGHT_OUTER) { if (nullNode == parentNode.getLastChild()) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } raiseNullThroughJoin(metadata, parentNode, parentNode.getFirstChild()); return null; } if (jt == JoinType.JOIN_FULL_OUTER) { if (nullNode == parentNode.getLastChild()) { raiseNullThroughJoin(metadata, parentNode, parentNode.getLastChild()); } else { raiseNullThroughJoin(metadata, parentNode, parentNode.getFirstChild()); } return null; } break; } case NodeConstants.Types.SET_OP: { boolean isLeftChild = parentNode.getFirstChild() == nullNode; SetQuery.Operation operation = (SetQuery.Operation)parentNode.getProperty(NodeConstants.Info.SET_OPERATION); boolean raiseOverSetOp = (operation == SetQuery.Operation.INTERSECT || (operation == SetQuery.Operation.EXCEPT && isLeftChild)); if (raiseOverSetOp) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } boolean isAll = parentNode.hasBooleanProperty(NodeConstants.Info.USE_ALL); if (isLeftChild) { PlanNode firstProject = NodeEditor.findNodePreOrder(parentNode, NodeConstants.Types.PROJECT); if (firstProject == null) { // will only happen if the other branch has only null nodes return raiseNullNode(rootNode, parentNode, nullNode, nodes); } List<Expression> newProjectSymbols = (List<Expression>)firstProject.getProperty(NodeConstants.Info.PROJECT_COLS); List<Expression> oldProjectSymbols = (List<Expression>)nullNode.getProperty(NodeConstants.Info.PROJECT_COLS); for (int i = 0; i < newProjectSymbols.size(); i++) { Expression newSes = newProjectSymbols.get(i); Expression oldSes = oldProjectSymbols.get(i); if (!(newSes instanceof Symbol) || !Symbol.getShortName(newSes).equals(Symbol.getShortName(oldSes))) { if (newSes instanceof AliasSymbol) { newSes = ((AliasSymbol)newSes).getSymbol(); } newProjectSymbols.set(i, new AliasSymbol(Symbol.getShortName(oldSes), newSes)); } } PlanNode sort = NodeEditor.findParent(parentNode, NodeConstants.Types.SORT, NodeConstants.Types.SOURCE); if (sort != null) { //correct the sort to the new columns as well OrderBy sortOrder = (OrderBy)sort.getProperty(NodeConstants.Info.SORT_ORDER); for (OrderByItem item : sortOrder.getOrderByItems()) { Expression sortElement = item.getSymbol(); sortElement = newProjectSymbols.get(oldProjectSymbols.indexOf(sortElement)); item.setSymbol(sortElement); } } PlanNode sourceNode = NodeEditor.findParent(parentNode, NodeConstants.Types.SOURCE); if (sourceNode != null && NodeEditor.findNodePreOrder(sourceNode, NodeConstants.Types.PROJECT) == firstProject) { SymbolMap symbolMap = (SymbolMap)sourceNode.getProperty(NodeConstants.Info.SYMBOL_MAP); symbolMap = SymbolMap.createSymbolMap(symbolMap.getKeys(), newProjectSymbols); sourceNode.setProperty(NodeConstants.Info.SYMBOL_MAP, symbolMap); } } NodeEditor.removeChildNode(parentNode, nullNode); PlanNode grandParent = parentNode.getParent(); if (!isAll) { //ensure that the new child is distinct PlanNode nestedSetOp = NodeEditor.findNodePreOrder(parentNode.getFirstChild(), NodeConstants.Types.SET_OP, NodeConstants.Types.SOURCE); if (nestedSetOp != null) { nestedSetOp.setProperty(NodeConstants.Info.USE_ALL, false); } else if (NodeEditor.findNodePreOrder(parentNode.getFirstChild(), NodeConstants.Types.DUP_REMOVE, NodeConstants.Types.SOURCE) == null) { parentNode.getFirstChild().addAsParent(NodeFactory.getNewNode(NodeConstants.Types.DUP_REMOVE)); } } if (grandParent == null) { PlanNode newRoot = parentNode.getFirstChild(); parentNode.removeChild(newRoot); return newRoot; } //remove the set op NodeEditor.removeChildNode(grandParent, parentNode); PlanNode sourceNode = NodeEditor.findParent(grandParent.getFirstChild(), NodeConstants.Types.SOURCE, NodeConstants.Types.SET_OP); if (sourceNode != null) { return RuleMergeVirtual.doMerge(sourceNode, rootNode, false, metadata, capFinder); } return null; } case NodeConstants.Types.GROUP: { //if there are grouping columns, then we can raise if (parentNode.hasCollectionProperty(NodeConstants.Info.GROUP_COLS)) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } break; //- the else case could be implemented, but it's a lot of work for little gain, since the null node can't raise higher } case NodeConstants.Types.PROJECT: { // check for project into PlanNode upperProject = NodeEditor.findParent(parentNode.getParent(), NodeConstants.Types.PROJECT, NodeConstants.Types.SOURCE); if (upperProject == null || upperProject.getProperty(NodeConstants.Info.INTO_GROUP) == null) { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } break; } case NodeConstants.Types.SOURCE: { PlanNode upperProject = parentNode.getParent(); if (upperProject != null && upperProject.getType() == NodeConstants.Types.PROJECT && upperProject.hasProperty(Info.INTO_GROUP)) { break; //an insert plan } return raiseNullNode(rootNode, parentNode, nullNode, nodes); } default: { return raiseNullNode(rootNode, parentNode, nullNode, nodes); } } return null; } private PlanNode raiseNullNode(PlanNode rootNode, PlanNode parentNode, PlanNode nullNode, List<PlanNode> nodes) { if (parentNode.getType() == NodeConstants.Types.SOURCE) { nullNode.getGroups().clear(); } else if (parentNode.getType() == NodeConstants.Types.PROJECT) { nullNode.setProperty(NodeConstants.Info.PROJECT_COLS, parentNode.getProperty(NodeConstants.Info.PROJECT_COLS)); } nullNode.addGroups(parentNode.getGroups()); parentNode.removeChild(nullNode); nodes.removeAll(NodeEditor.findAllNodes(parentNode, NodeConstants.Types.NULL)); if (parentNode.getParent() != null) { parentNode.getParent().replaceChild(parentNode, nullNode); } else { rootNode = nullNode; } return rootNode; } /** * Given a joinNode that should be an outer join and a null node as one of its children, replace elements in * the current frame from the null node groups with null values * * @param metadata * @param joinNode * @param nullNode * @throws QueryPlannerException * @throws QueryMetadataException * @throws TeiidComponentException */ static void raiseNullThroughJoin(QueryMetadataInterface metadata, PlanNode joinNode, PlanNode nullNode) throws QueryPlannerException, QueryMetadataException, TeiidComponentException { Assertion.assertTrue(joinNode.getType() == NodeConstants.Types.JOIN); Assertion.assertTrue(nullNode.getType() == NodeConstants.Types.NULL); Assertion.assertTrue(nullNode.getParent() == joinNode); PlanNode frameStart = joinNode.getParent(); NodeEditor.removeChildNode(joinNode, nullNode); NodeEditor.removeChildNode(joinNode.getParent(), joinNode); for (GroupSymbol group : nullNode.getGroups()) { Map<ElementSymbol, Expression> nullSymbolMap = FrameUtil.buildSymbolMap(group, null, metadata); FrameUtil.convertFrame(frameStart, group, null, nullSymbolMap, metadata); } } public String toString() { return "RaiseNull"; //$NON-NLS-1$ } }