/* * 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.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.modeshape.common.annotation.Immutable; import org.modeshape.jcr.query.QueryContext; import org.modeshape.jcr.query.model.Column; import org.modeshape.jcr.query.model.SelectorName; import org.modeshape.jcr.query.plan.CanonicalPlanner; import org.modeshape.jcr.query.plan.PlanNode; import org.modeshape.jcr.query.plan.PlanNode.Property; import org.modeshape.jcr.query.plan.PlanNode.Type; import org.modeshape.jcr.query.plan.PlanUtil; import org.modeshape.jcr.query.validate.Schemata; import org.modeshape.jcr.query.validate.Schemata.Table; import org.modeshape.jcr.query.validate.Schemata.View; /** * An {@link OptimizerRule optimizer rule} that replaces any SOURCE nodes that happen to be {@link View views}. This rewriting * changes all of the elements of the plan that reference the SOURCE and it's columns, including criteria, project nodes, etc. * <p> * For example, here is the portion of a plan that uses a single SOURCE that is defined to use a view. * * <pre> * ... * | * SOURCE1 * </pre> * * This same SOURCE node is then replaced with the view's definition: * * <pre> * ... * | * 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 * </pre> * * </p> */ @Immutable public class ReplaceViews implements OptimizerRule { public static final ReplaceViews INSTANCE = new ReplaceViews(); @Override public PlanNode execute( QueryContext context, PlanNode plan, LinkedList<OptimizerRule> ruleStack ) { CanonicalPlanner planner = new CanonicalPlanner(); // Prepare the maps that will record the old-to-new mappings from the old view SOURCE nodes to the table SOURCEs ... // For each of the SOURCE nodes ... Schemata schemata = context.getSchemata(); Set<PlanNode> processedSources = new HashSet<PlanNode>(); boolean foundViews = false; do { foundViews = false; for (PlanNode sourceNode : plan.findAllAtOrBelow(Type.SOURCE)) { if (processedSources.contains(sourceNode)) continue; processedSources.add(sourceNode); // Resolve the node to find the definition in the schemata ... SelectorName tableName = sourceNode.getProperty(Property.SOURCE_NAME, SelectorName.class); SelectorName tableAlias = sourceNode.getProperty(Property.SOURCE_ALIAS, SelectorName.class); Table table = schemata.getTable(tableName); if (table instanceof View) { View view = (View)table; PlanNode viewPlan = planner.createPlan(context, view.getDefinition()); if (viewPlan == null) continue; // there were likely errors when creating the plan // If the view doesn't have an alias, or if the view's alias doesn't match the table's name/alias ... PlanNode viewProjectNode = viewPlan.findAtOrBelow(Type.PROJECT); if (viewProjectNode.getSelectors().size() == 1) { SelectorName tableAliasOrName = tableAlias != null ? tableAlias : tableName; SelectorName viewAlias = viewProjectNode.getSelectors().iterator().next(); // Replace the view's alias ... Map<SelectorName, SelectorName> replacements = Collections.singletonMap(viewAlias, tableAliasOrName); PlanUtil.replaceReferencesToRemovedSource(context, viewPlan, replacements); if (!context.getHints().validateColumnExistance) { // Find the next highest PROJECT node above the source ... PlanNode project = sourceNode.findAncestor(Type.PROJECT); if (project != null) { List<Column> projectedColumns = project.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class); // There may be columns that don't appear in the source, so make sure they are there ... viewPlan = PlanUtil.addMissingProjectColumns(context, viewProjectNode, projectedColumns); assert viewPlan != null; } } } // Insert the view plan under the parent SOURCE node ... sourceNode.addLastChild(viewPlan); // Remove the source node ... sourceNode.extractFromParent(); // // Replace the original view's name with the name/alias ... PlanNode parent = viewPlan.getParent(); if (parent != null) { PlanUtil.ColumnMapping aliasMappings = null; if (tableAlias != null) { aliasMappings = PlanUtil.createMappingForAliased(tableAlias, view, viewPlan); PlanUtil.replaceViewReferences(context, parent, aliasMappings); } PlanUtil.ColumnMapping viewMappings = PlanUtil.createMappingFor(view, viewPlan); PlanUtil.replaceViewReferences(context, parent, viewMappings); } if (viewPlan.is(Type.PROJECT)) { // The PROJECT from the plan may actually not be needed if there is another PROJECT above it ... PlanNode node = viewPlan.getParent(); while (node != null) { if (node.isOneOf(Type.JOIN)) break; if (node.is(Type.PROJECT) && viewPlan.getSelectors().containsAll(node.getSelectors())) { viewPlan.extractFromParent(); break; } node = node.getParent(); } } foundViews = true; } } } while (foundViews); if (foundViews) { // We'll need to try to push up criteria from the join, but we only should do this after this rule // is completely done ... if (!(ruleStack.getFirst() instanceof RaiseSelectCriteria)) { ruleStack.addFirst(RaiseSelectCriteria.INSTANCE); ruleStack.addFirst(PushSelectCriteria.INSTANCE); } // We re-wrote at least one SOURCE, but the resulting plan tree for the view could actually reference // other views. Therefore, re-run this rule ... ruleStack.addFirst(this); } return plan; } @Override public String toString() { return getClass().getSimpleName(); } }