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

org.modeshape.jcr.query.optimize.PushSelectCriteria Maven / Gradle / Ivy

There is a newer version: 5.4.1.Final
Show newest version
/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr.query.optimize;

import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.model.Constraint;
import org.modeshape.jcr.query.model.JoinType;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.plan.PlanNode;
import org.modeshape.jcr.query.plan.PlanNode.Property;
import org.modeshape.jcr.query.plan.PlanNode.Type;

/**
 * An {@link OptimizerRule optimizer rule} that attempts to push the criteria nodes in a canonical plan down as far as possible.
 * 

* For example, here is a single-access plan before: * *

 *          ...
 *           |
 *        PROJECT      with the list of columns being SELECTed
 *           |
 *        SELECT1
 *           |         One or more SELECT plan nodes that each have
 *        SELECT2      a single non-join constraint that are then all AND-ed
 *           |         together
 *        SELECTn
 *           |
 *        ACCESS
 *           |
 *        SOURCE
 * 
* * And after: * *
 *          ...
 *           |
 *        ACCESS
 *           |
 *        PROJECT      with the list of columns being SELECTed
 *           |
 *        SELECT1
 *           |         One or more SELECT plan nodes that each have
 *        SELECT2      a single non-join constraint that are then all AND-ed
 *           |         together
 *        SELECTn
 *           |
 *        SOURCE
 * 
* * Here is another case, where multiple SELECT nodes above a simple JOIN and where each SELECT node applies to one or more of the * SOURCE nodes (via the named selectors). Each SELECT node that applies to a single selector will get pushed toward that source, * but will have the same order relative to other SELECT nodes also pushed toward that SOURCE. However, this rules does not push * SELECT nodes that apply to multiple selectors. *

*

* Before: * *

 *          ...
 *           |
 *        PROJECT ('s1','s2')      with the list of columns being SELECTed (from 's1' and 's2' selectors)
 *           |
 *        SELECT1 ('s1')
 *           |                     One or more SELECT plan nodes that each have
 *        SELECT2 ('s2')           a single non-join constraint that are then all AND-ed
 *           |                     together, and that each have the selector(s) they apply to
 *        SELECT3 ('s1','s2')
 *           |
 *        SELECT4 ('s1')
 *           |
 *         JOIN ('s1','s2')
 *        /     \
 *       /       \
 *   ACCESS     ACCESS
 *    ('s1')    ('s2')
 *     |           |
 *   PROJECT    PROJECT
 *    ('s1')    ('s2')
 *     |           |
 *   SOURCE     SOURCE
 *    ('s1')    ('s2')
 * 
* * And after: * *
 *          ...
 *           |
 *        PROJECT ('s1','s2')      with the list of columns being SELECTed (from 's1' and 's2' selectors)
 *           |
 *        SELECT3 ('s1','s2')      Any SELECT plan nodes that apply to multiple selectors are left above
 *           |                     the ACCESS nodes.
 *         JOIN ('s1','s2')
 *        /     \
 *       /       \
 *   ACCESS     ACCESS
 *   ('s1')     ('s2')
 *     |           |
 *  PROJECT     PROJECT
 *   ('s1')     ('s2')
 *     |           |
 *  SELECT1     SELECT2
 *   ('s1')     ('s2')
 *     |           |
 *  SELECT4     SOURCE
 *   ('s1')     ('s2')
 *     |
 *   SOURCE
 *   ('s1')
 * 
* *

*

* Also, any SELECT that applies to one side of an equi-join will be applied to both sides of the JOIN. *

*/ @Immutable public class PushSelectCriteria implements OptimizerRule { public static final PushSelectCriteria INSTANCE = new PushSelectCriteria(); private static final Set ORIGINATING_TYPES = Collections.unmodifiableSet(EnumSet.of(Type.NULL, Type.SOURCE, Type.JOIN, Type.SET_OPERATION)); @Override public PlanNode execute( QueryContext context, PlanNode plan, LinkedList ruleStack ) { // Create set of nodes that no longer need to be considered Set deadNodes = new HashSet(); // Loop while criteria nodes are still being moved ... boolean movedSomeNode = true; while (movedSomeNode) { // Reset flag to false for this iteration movedSomeNode = false; // Find all of the criteria (SELECT) nodes that can be pushed ... List criteriaNodes = plan.findAllAtOrBelow(Type.SELECT); // Find all of the NULL, SOURCE, SET_OPERATION or JOIN nodes, ordered correctly; we'll use this // to look for the node on which each criteria can apply ... List originatingNodes = plan.findAllAtOrBelow(ORIGINATING_TYPES); // Starting with the lowest one first ... Collections.reverse(criteriaNodes); for (PlanNode criteriaNode : criteriaNodes) { // Skip any node we've already tried and failed to move ... if (deadNodes.contains(criteriaNode)) continue; // Find the first originating node that has all of the required selectors for this criteria ... PlanNode originatingNode = findOriginatingNode(criteriaNode, originatingNodes); if (originatingNode == null || originatingNode == criteriaNode) { deadNodes.add(criteriaNode); continue; } // Try to push the criteria node down ... if (!pushTowardsOriginatingNode(criteriaNode, originatingNode)) { // criteria node could not be moved ... deadNodes.add(criteriaNode); continue; } // The criteria node was indeed moved, but it may need to be adjusted ... boolean adjusted = false; switch (originatingNode.getType()) { case SOURCE: break; case JOIN: if (!criteriaNode.hasAncestorOfType(Type.ACCESS)) { // Try to push down the join criteria (only when above ACCESS nodes) ... adjusted = pushDownJoinCriteria(criteriaNode, originatingNode); } break; default: // Nothing to change ... } if (adjusted) { // We changed something, so make sure we go through the loop again ... movedSomeNode = true; } else { // Nothing was changed from the push-down, so consider this criteria node as processed ... deadNodes.add(criteriaNode); } } } return plan; } /** * Attempt to push down criteria that applies to the JOIN as additional constraints on the JOIN itself. * * @param criteriaNode the SELECT node; may not be null * @param joinNode the JOIN node; may not be null * @return true if the criteria was pushed down, or false otherwise */ protected boolean pushDownJoinCriteria( PlanNode criteriaNode, PlanNode joinNode ) { JoinType joinType = (JoinType)joinNode.getProperty(Property.JOIN_TYPE); switch (joinType) { case CROSS: joinNode.setProperty(Property.JOIN_TYPE, JoinType.INNER); moveCriteriaIntoOnClause(criteriaNode, joinNode); break; case INNER: moveCriteriaIntoOnClause(criteriaNode, joinNode); break; default: // This is where we could attempt to optimize the join type ... // if (optimizeJoinType(criteriaNode, joinNode) == JoinType.INNER) { // // The join type has changed ... // moveCriteriaIntoOnClause(criteriaNode, joinNode); // return true; // since the join type has changed ... // } } return false; } /** * Move the criteria that applies to the join to be included in the actual join criteria. * * @param criteriaNode the SELECT node; may not be null * @param joinNode the JOIN node; may not be null */ private void moveCriteriaIntoOnClause( PlanNode criteriaNode, PlanNode joinNode ) { List constraints = joinNode.getPropertyAsList(Property.JOIN_CONSTRAINTS, Constraint.class); Constraint criteria = criteriaNode.getProperty(Property.SELECT_CRITERIA, Constraint.class); // since the parser uses EMPTY_LIST, check for size 0 also if (constraints == null || constraints.isEmpty()) { constraints = new LinkedList(); joinNode.setProperty(Property.JOIN_CONSTRAINTS, constraints); } if (!constraints.contains(criteria)) { constraints.add(criteria); if (criteriaNode.hasBooleanProperty(Property.IS_DEPENDENT)) { joinNode.setProperty(Property.IS_DEPENDENT, Boolean.TRUE); } } criteriaNode.extractFromParent(); } /** * Find the first node that has all of the same {@link PlanNode#getSelectors() selectors} as the supplied criteria. * * @param criteriaNode the criteria * @param originatingNodes the list of nodes to search * @return the first (highest) node that is uses all of the same selectors as the criteria, or null if no such node could be * found */ protected PlanNode findOriginatingNode( PlanNode criteriaNode, List originatingNodes ) { Set requiredSelectors = criteriaNode.getSelectors(); if (requiredSelectors.isEmpty()) return criteriaNode; // first look for originating nodes that exactly match the required selectors ... for (PlanNode originatingNode : originatingNodes) { if (!criteriaNode.isAbove(originatingNode)) continue; if (originatingNode.getSelectors().equals(requiredSelectors)) return originatingNode; } // Nothing matched exactly, so can we push down to a node that contain all of the required selectors ... for (PlanNode originatingNode : originatingNodes) { if (originatingNode.getSelectors().containsAll(requiredSelectors)) return originatingNode; } return null; } /** * Push the criteria node as close to the originating node as possible. In general, the criteria node usually ends up being * moved to be a parent of the supplied originating node, except in a couple of cases: *
    *
  • There are already criteria nodes immediately above the originating node; in this case, the supplied criteria node is * placed above all these existing criteria nodes.
  • *
  • The originating node is below a JOIN node that is itself below an ACCESS node; in this case, the criteria node is * placed immediately above the JOIN node.
  • *
  • The originating node is below a LIMIT with a single SORT child node; in this case, the criteria node is placed * immediately above the LIMIT node.
  • *
* * @param criteriaNode the criteria node that is to be pushed down; may not be null * @param originatingNode the target node that represents the node above which the criteria node should be inserted; may not * be null * @return true if the criteria node was pushed down, or false if the criteria node could not be pushed down */ protected boolean pushTowardsOriginatingNode( PlanNode criteriaNode, PlanNode originatingNode ) { // To keep things stable, 'originatingNode' should be the top-most SELECT (criteria) node above a SOURCE ... while (originatingNode.getParent().getType() == Type.SELECT) { originatingNode = originatingNode.getParent(); if (originatingNode == criteriaNode) return false; } // Find out the best node above which the criteria node should be placed ... PlanNode bestChild = findBestChildForCriteria(criteriaNode, originatingNode); if (bestChild == criteriaNode) return false; criteriaNode.extractFromParent(); bestChild.insertAsParent(criteriaNode); assert atBoundary(criteriaNode, originatingNode); return true; } protected PlanNode findBestChildForCriteria( PlanNode criteriaNode, PlanNode originatingNode ) { // Walk the nodes, from the criteria node down to the originating node ... for (PlanNode node : criteriaNode.getPathTo(originatingNode)) { // Check the node to see if there is any reason why the node cannot be pushed if (node.getType() == Type.JOIN) { // Pushing below a JOIN is not necessary under an ACCESS node if (node.hasAncestorOfType(Type.ACCESS)) return node; } else if (node.getType() == Type.LIMIT) { // Don't push below a LIMIT above a SORT ... if (node.getChildCount() == 1 && node.getFirstChild().getType() == Type.SORT) { return node; } } } return originatingNode; } /** * Determine whether all of the nodes between the criteria node and its originating node are criteria (SELECT) nodes. * * @param criteriaNode the criteria node; may not be null * @param originatingNode the originating node * @return true if all nodes between the criteria and originating nodes are SELECT nodes */ protected boolean atBoundary( PlanNode criteriaNode, PlanNode originatingNode ) { // Walk from source node to critNode to check each intervening node PlanNode currentNode = originatingNode.getParent(); while (currentNode != criteriaNode) { if (currentNode.getType() != Type.SELECT) return false; currentNode = currentNode.getParent(); } return true; } @Override public String toString() { return getClass().getSimpleName(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy