/*
* 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidException;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
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.NodeEditor;
import org.teiid.query.optimizer.relational.plantree.PlanNode;
import org.teiid.query.rewriter.QueryRewriter;
import org.teiid.query.sql.lang.CompareCriteria;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.lang.IsNullCriteria;
import org.teiid.query.sql.lang.JoinType;
import org.teiid.query.sql.symbol.Constant;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.visitor.EvaluatableVisitor;
import org.teiid.query.sql.visitor.GroupsUsedByElementsVisitor;
import org.teiid.query.util.CommandContext;
/**
* For each join node this rule will find the set of criteria allowed to influence the join (the join criteria, and inner
* side criteria on non full outer joins) and builds new criteria based upon the equality relationships found.
*
* Equality relationships look like element symbol = expression regardless of whether they are from select or join criteria
*
* Upon successfully changing a multi group join criteria into another expression with fewer groups, the original criteria
* will be replace with the new criteria in the on clause.
*
* RulePushNonJoinCriteia and CopyCriteria will be run again after this rule if any new join criteria is created.
*
* This rule is not allowed to run exhaustively by the setting of the copied property on criteria nodes. It also will not
* discover all possible relationships, only those that can be discovered quickly.
*/
public final class RuleCopyCriteria implements OptimizerRule {
/**
* Execute the rule as described in the class comments.
* @param plan Incoming query plan, may be modified during method and may be returned from method
* @param metadata Metadata source
* @param rules Rules from optimizer rule stack, may be manipulated during method
* @return Updated query plan if rule fired, else original query plan
*/
public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata, CapabilitiesFinder capFinder, RuleStack rules, AnalysisRecord analysisRecord, CommandContext context)
throws QueryPlannerException, TeiidComponentException {
List<PlanNode> critNodes = NodeEditor.findAllNodes(plan, NodeConstants.Types.SELECT | NodeConstants.Types.JOIN);
boolean shouldRun = false;
for (PlanNode critNode : critNodes) {
if (!critNode.hasBooleanProperty(NodeConstants.Info.IS_COPIED)) {
shouldRun = true;
break;
}
}
if (!shouldRun) {
return plan;
}
if (tryToCopy(plan, new Set[2], metadata, false)) {
//Push any newly created criteria nodes and try to copy them afterwards
rules.push(RuleConstants.COPY_CRITERIA);
rules.push(RuleConstants.RAISE_ACCESS);
rules.push(new RulePushNonJoinCriteria(false));
}
//mark the old criteria nodes as copied. this will prevent RulePushSelectCriteria from considering them again
for (PlanNode critNode : critNodes) {
critNode.setProperty(NodeConstants.Info.IS_COPIED, Boolean.TRUE);
}
return plan;
}
/**
* Given a criteria and a map of elements to values try to create a new single group criteria
*
* If the new criteria does not have exactly one group or already exists in the combined criteria,
* it will not be added.
*
* @param crit
* @param tgtMap
* @param joinCriteria
* @param combinedCriteria
* @return number of remaining groups if the copy was successful
*/
private Integer copyCriteria(Criteria crit,
Map<Expression, Expression> tgtMap,
List<Criteria> joinCriteria,
Set<Criteria> combinedCriteria,
boolean checkForGroupReduction,
QueryMetadataInterface metadata,
boolean underAccess) {
int startGroups = GroupsUsedByElementsVisitor.getGroups(crit).size();
Criteria tgtCrit = (Criteria) crit.clone();
try {
tgtCrit = FrameUtil.convertCriteria(tgtCrit, tgtMap, metadata, true);
} catch (QueryPlannerException err) {
LogManager.logDetail(LogConstants.CTX_QUERY_PLANNER, err, "Could not remap target criteria in RuleCopyCriteria"); //$NON-NLS-1$
return null;
}
if (tgtCrit instanceof IsNullCriteria && ((IsNullCriteria)tgtCrit).isNegated()) {
return null;
}
int endGroups = GroupsUsedByElementsVisitor.getGroups(tgtCrit).size();
if (checkForGroupReduction) {
if (endGroups >= startGroups) {
return null;
}
} else if (endGroups > startGroups) {
return null;
}
boolean isNew = combinedCriteria.add(tgtCrit);
if (underAccess) {
boolean use = false;
if (isNew && !checkForGroupReduction) {
if (endGroups == 1) {
use = true;
} else if (tgtCrit instanceof CompareCriteria) {
CompareCriteria cc = (CompareCriteria)tgtCrit;
int leftGroups = GroupsUsedByElementsVisitor.getGroups(cc.getLeftExpression()).size();
if (leftGroups == endGroups || leftGroups == 0) {
use = true;
}
}
}
if (!use) {
return null;
}
}
//if this is unique or it a duplicate but reduced a current join conjunct, return true
if (isNew) {
joinCriteria.add(tgtCrit);
if (tgtCrit instanceof CompareCriteria) {
CompareCriteria cc = (CompareCriteria)tgtCrit;
if (!EvaluatableVisitor.willBecomeConstant(cc.getRightExpression()) &&
!EvaluatableVisitor.willBecomeConstant(cc.getRightExpression())) {
((CompareCriteria)tgtCrit).setOptional(true);
}
}
return endGroups;
} else if (checkForGroupReduction && endGroups < 2) {
return endGroups;
}
return null;
}
/**
* Recursively tries to copy criteria across join nodes. toCopy will contain only the single group criteria
* that has not yet been copied. allCriteria will contain all criteria present at the join that can effect
* copying.
*
* @param node
* @return true if criteria has been created
*/
private boolean tryToCopy(PlanNode node, Set<Criteria>[] criteriaInfo, QueryMetadataInterface metadata, boolean underAccess) {
boolean changedTree = false;
if (node == null) {
return false;
}
//visit join nodes in order
if (node.getType() == NodeConstants.Types.JOIN) {
JoinType jt = (JoinType)node.getProperty(NodeConstants.Info.JOIN_TYPE);
if (jt == JoinType.JOIN_FULL_OUTER) {
return visitChildern(node, criteriaInfo, changedTree, metadata, underAccess);
}
Set<Criteria>[] leftChildCriteria = new Set[2];
Set<Criteria>[] rightChildCriteria = new Set[2];
changedTree |= tryToCopy(node.getFirstChild(), leftChildCriteria, metadata, underAccess);
changedTree |= tryToCopy(node.getLastChild(), rightChildCriteria, metadata, underAccess);
List<Criteria> joinCrits = (List<Criteria>) node.getProperty(NodeConstants.Info.JOIN_CRITERIA);
Set<Criteria> combinedCriteria = null;
if (joinCrits != null) {
combinedCriteria = new LinkedHashSet<Criteria>(joinCrits);
combinedCriteria.addAll(leftChildCriteria[1]);
combinedCriteria.addAll(rightChildCriteria[1]);
}
//combine the criteria
leftChildCriteria[0].addAll(rightChildCriteria[0]);
leftChildCriteria[1].addAll(rightChildCriteria[1]);
//set the applicable criteria
criteriaInfo[0] = leftChildCriteria[0];
//set the all criteria
criteriaInfo[1] = leftChildCriteria[1];
//there's no join criteria here, so just let the criteria go up
if (jt == JoinType.JOIN_CROSS) {
return changedTree;
}
Set<Criteria> toCopy = criteriaInfo[0];
Set<Criteria> allCriteria = criteriaInfo[1];
if (joinCrits != null && !joinCrits.isEmpty()) {
List<Criteria> newJoinCrits = new LinkedList<Criteria>();
//we don't want to continue discovery since that could be recursive
Map<Expression, Expression> srcToTgt = buildElementMap(joinCrits, node.hasBooleanProperty(NodeConstants.Info.IS_COPIED)?null:newJoinCrits, combinedCriteria, metadata, underAccess);
changedTree |= !newJoinCrits.isEmpty();
if (!toCopy.isEmpty()) {
changedTree |= createCriteria(false, toCopy, combinedCriteria, srcToTgt, newJoinCrits, metadata, underAccess);
srcToTgt = buildElementMap(allCriteria, null, null, metadata, underAccess);
changedTree |= createCriteria(true, joinCrits, combinedCriteria, srcToTgt, newJoinCrits, metadata, underAccess);
}
joinCrits.addAll(newJoinCrits);
}
//before returning, filter out criteria that cannot go above the join node
if (jt == JoinType.JOIN_RIGHT_OUTER) {
criteriaInfo[0].removeAll(leftChildCriteria[0]);
criteriaInfo[1].removeAll(leftChildCriteria[1]);
} else if (jt == JoinType.JOIN_LEFT_OUTER) {
criteriaInfo[0].removeAll(rightChildCriteria[0]);
criteriaInfo[1].removeAll(rightChildCriteria[1]);
} else if (node.getSubqueryContainers().isEmpty()) {
if (!node.hasBooleanProperty(NodeConstants.Info.IS_COPIED)) {
toCopy.addAll(joinCrits);
}
allCriteria.addAll(joinCrits);
}
return changedTree;
}
changedTree = visitChildern(node, criteriaInfo, changedTree, metadata, underAccess);
//visit select nodes on the way back up
switch (node.getType()) {
case NodeConstants.Types.SELECT:
{
if (criteriaInfo[0] != null) {
visitSelectNode(node, criteriaInfo[0], criteriaInfo[1]);
}
break;
}
//clear the criteria when hitting the following
case NodeConstants.Types.NULL:
case NodeConstants.Types.SOURCE:
case NodeConstants.Types.GROUP:
case NodeConstants.Types.SET_OP:
case NodeConstants.Types.PROJECT:
{
if (criteriaInfo[0] == null) {
criteriaInfo[0] = new LinkedHashSet<Criteria>();
criteriaInfo[1] = new LinkedHashSet<Criteria>();
} else {
criteriaInfo[0].clear();
criteriaInfo[1].clear();
}
break;
}
}
return changedTree;
}
private boolean createCriteria(boolean copyingJoinCriteria, Collection<Criteria> toCopy,
Set<Criteria> combinedCriteria,
Map<Expression, Expression> srcToTgt,
List<Criteria> newJoinCrits,
QueryMetadataInterface metadata, boolean underAccess) {
boolean changedTree = false;
if (srcToTgt.size() == 0) {
return changedTree;
}
Iterator<Criteria> i = toCopy.iterator();
while (i.hasNext()) {
Criteria crit = i.next();
Integer endGroups = copyCriteria(crit, srcToTgt, newJoinCrits, combinedCriteria, copyingJoinCriteria, metadata, underAccess);
if (endGroups != null) {
changedTree = true;
if (copyingJoinCriteria && endGroups < 2) {
if (crit instanceof CompareCriteria) {
CompareCriteria cc = (CompareCriteria)crit;
//don't remove theta criteria, just mark it as optional
cc.setOptional(null);
continue;
}
i.remove();
}
}
}
return changedTree;
}
private void visitSelectNode(PlanNode node,
Set<Criteria> toCopy,
Set<Criteria> allCriteria) {
//First examine criteria in critNode for suitability
Criteria crit = (Criteria) node.getProperty(NodeConstants.Info.SELECT_CRITERIA);
if(node.getGroups().size() == 1) {
List<Criteria> crits = Criteria.separateCriteriaByAnd(crit);
if(!node.hasBooleanProperty(NodeConstants.Info.IS_HAVING) && node.getSubqueryContainers().isEmpty()) {
if (!node.hasBooleanProperty(NodeConstants.Info.IS_COPIED)) {
toCopy.addAll(crits);
}
allCriteria.addAll(crits);
}
}
}
private boolean visitChildern(PlanNode node,
Set<Criteria>[] criteriaInfo,
boolean changedTree,
QueryMetadataInterface metadata, boolean underAccess) {
if (node.getChildCount() > 0) {
underAccess |= node.getType() == NodeConstants.Types.ACCESS;
List<PlanNode> children = node.getChildren();
for (int i = 0; i < children.size(); i++) {
PlanNode childNode = children.get(i);
changedTree |= tryToCopy(childNode, i==0?criteriaInfo:new Set[2], metadata, underAccess);
}
}
return changedTree;
}
/**
* Construct a mapping of element symbol to value map based upon equality CompareCriteria in crits
*
* @param crits
* @param newJoinCrits
* @param metadata
* @return
*/
Map<Expression, Expression> buildElementMap(Collection<Criteria> crits, List<Criteria> newJoinCrits, Set<Criteria> allCriteria, QueryMetadataInterface metadata, boolean underAccess) {
Map<Expression, Expression> srcToTgt = null;
for (Iterator<Criteria> iter = crits.iterator(); iter.hasNext();) {
Criteria theCrit = iter.next();
if (theCrit instanceof IsNullCriteria) {
IsNullCriteria isNull = (IsNullCriteria)theCrit;
if (!isNull.isNegated() && isNull.getExpression() instanceof ElementSymbol) {
if (srcToTgt == null) {
srcToTgt = new HashMap<Expression, Expression>();
}
srcToTgt.put(isNull.getExpression(), new Constant(null, isNull.getExpression().getType()));
}
continue;
}
if(!(theCrit instanceof CompareCriteria)) {
continue;
}
CompareCriteria crit = (CompareCriteria) theCrit;
if (crit.getOperator() == CompareCriteria.EQ) {
if (srcToTgt == null) {
srcToTgt = new HashMap<Expression, Expression>();
}
Expression oldValue = srcToTgt.put(crit.getLeftExpression(), crit.getRightExpression());
boolean removed = false;
if (checkWithinJoin(crit, newJoinCrits, allCriteria, oldValue, crit.getRightExpression(), metadata, underAccess)) {
iter.remove();
removed = true;
}
oldValue = srcToTgt.put(crit.getRightExpression(), crit.getLeftExpression());
if (checkWithinJoin(crit, newJoinCrits, allCriteria, oldValue, crit.getLeftExpression(), metadata, underAccess) && !removed) {
iter.remove();
}
}
}
if (srcToTgt == null) {
return Collections.emptyMap();
}
return srcToTgt;
}
/**
* @return true if the original crit can be removed
*/
private boolean checkWithinJoin(CompareCriteria crit, List<Criteria> newJoinCrits, Set<Criteria> allCriteria,
Expression oldValue, Expression left, QueryMetadataInterface metadata, boolean underAccess) {
if (newJoinCrits == null || oldValue == null) {
return false;
}
if (oldValue.equals(left)) {
return true;
}
Criteria newCrit = new CompareCriteria((Expression)left.clone(), CompareCriteria.EQ, (Expression)oldValue.clone());
try {
newCrit = QueryRewriter.rewriteCriteria(newCrit, null, metadata);
} catch (TeiidException e) {
LogManager.logDetail(LogConstants.CTX_QUERY_PLANNER, e, "Could not remap target criteria in RuleCopyCriteria"); //$NON-NLS-1$
return false;
}
if (allCriteria.add(newCrit)) {
if (underAccess && GroupsUsedByElementsVisitor.getGroups(newCrit).size() > 1) {
return false;
}
if (newCrit instanceof CompareCriteria) {
((CompareCriteria)newCrit).setOptional(true);
}
newJoinCrits.add(newCrit);
}
if (!GroupsUsedByElementsVisitor.getGroups(crit.getLeftExpression()).isEmpty() && !GroupsUsedByElementsVisitor.getGroups(crit.getRightExpression()).isEmpty()
&& (GroupsUsedByElementsVisitor.getGroups(left).isEmpty() || GroupsUsedByElementsVisitor.getGroups(oldValue).isEmpty())) {
crit.setOptional(null); //the original has been simplified
}
return false;
}
public String toString() {
return "CopyCriteria"; //$NON-NLS-1$
}
}