/*
* 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.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.modeshape.jcr.query.QueryContext;
import org.modeshape.jcr.query.model.Column;
import org.modeshape.jcr.query.model.Ordering;
import org.modeshape.jcr.query.model.PropertyValue;
import org.modeshape.jcr.query.model.ReferenceValue;
import org.modeshape.jcr.query.model.SelectorName;
import org.modeshape.jcr.query.model.Visitor;
import org.modeshape.jcr.query.model.Visitors;
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} that adds any missing columns required by the ordering specifications to the SORT node's PROJECT, and
* to the appropriate access nodes.
*/
public class AddOrderingColumnsToSources implements OptimizerRule {
public static final AddOrderingColumnsToSources INSTANCE = new AddOrderingColumnsToSources();
@Override
public PlanNode execute( QueryContext context,
PlanNode plan,
LinkedList<OptimizerRule> ruleStack ) {
final boolean includeSourceName = context.getHints().qualifyExpandedColumnNames;
// For each of the SORT nodes ...
for (PlanNode sortNode : plan.findAllAtOrBelow(Type.SORT)) {
// Get the list of property and reference expressions that are used in the Ordering instances, using a visitor. Other
// kinds of
// dynamic operands in Ordering instances will not need to change the columns
final Set<Column> sortColumns = new HashSet<Column>();
Visitor columnVisitor = new Visitors.AbstractVisitor() {
@Override
public void visit( PropertyValue prop ) {
sortColumns.add(columnFor(prop.selectorName(), prop.getPropertyName(), includeSourceName));
}
@Override
public void visit( ReferenceValue ref ) {
sortColumns.add(columnFor(ref.selectorName(), ref.getPropertyName(), includeSourceName));
}
};
List<Object> orderBys = sortNode.getPropertyAsList(Property.SORT_ORDER_BY, Object.class);
if (orderBys != null && !orderBys.isEmpty()) {
for (Object ordering : orderBys) {
if (ordering instanceof Ordering) {
Visitors.visitAll((Ordering)ordering, columnVisitor);
}
}
}
// Add each of the sort columns to the appropriate PROJECT nodes in the subtrees of this plan ...
Set<Column> columnsOnlyForSort = new HashSet<Column>();
for (Column sortColumn : sortColumns) {
if (addSortColumn(context, sortNode, sortColumn)) columnsOnlyForSort.add(sortColumn);
}
// If any columns were added only for the sort, we need to insert a PROJECT without the sort-only column(s)...
if (!columnsOnlyForSort.isEmpty()) {
// Find the existing project below the sort ...
PlanNode existingProject = sortNode.findAtOrBelow(Type.PROJECT);
List<Column> columns = existingProject.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class);
List<String> types = existingProject.getPropertyAsList(Property.PROJECT_COLUMN_TYPES, String.class);
columns = new ArrayList<Column>(columns);
types = new ArrayList<String>(types);
for (Column sortOnlyColumn : columnsOnlyForSort) {
int index = columns.indexOf(sortOnlyColumn);
if (index >= 0) {
columns.remove(index);
types.remove(index);
}
}
// Determine the minimum selectors ...
Set<SelectorName> selectors = new HashSet<SelectorName>();
for (Column column : columns) {
selectors.add(column.selectorName());
}
// Now create a new PROJECT that wraps the SORT to remove the column(s) needed by the SORT ...
PlanNode newProject = new PlanNode(Type.PROJECT);
newProject.addSelectors(selectors);
newProject.setProperty(Property.PROJECT_COLUMNS, columns);
newProject.setProperty(Property.PROJECT_COLUMN_TYPES, types);
// And insert the new PROJECT node ...
sortNode.insertAsParent(newProject);
if (plan == sortNode) plan = newProject;
}
}
return plan;
}
/**
* Make sure that the supplied column is included in the {@link Property#PROJECT_COLUMNS projected columns} on the supplied
* plan node or its children.
*
* @param context the query context; may not be null
* @param node the query plan node
* @param sortColumn the column required by the sort
* @return true if the sort column was added, or false if it was already in the {@link Property#PROJECT_COLUMNS projected
* columns}
*/
protected boolean addSortColumn( QueryContext context,
PlanNode node,
Column sortColumn ) {
boolean added = false;
if (node.getSelectors().contains(sortColumn.selectorName())) {
// Get the existing projected columns ...
List<Column> columns = node.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class);
List<String> types = node.getPropertyAsList(Property.PROJECT_COLUMN_TYPES, String.class);
if (columns != null && addIfMissing(context, sortColumn, columns, types)) {
node.setProperty(Property.PROJECT_COLUMNS, columns);
node.setProperty(Property.PROJECT_COLUMN_TYPES, types);
added = true;
}
}
// Apply recursively ...
for (PlanNode child : node) {
addSortColumn(context, child, sortColumn);
}
if (node.is(Type.SORT)) {
// Get the child node, which should be a PROJECT ...
PlanNode child = node.findAtOrBelow(Type.PROJECT);
if (child != null && !child.getSelectors().contains(sortColumn.selectorName())) {
// Make sure the PROJECT includes the missing columns, even when the selector is not included ...
List<Column> columns = child.getPropertyAsList(Property.PROJECT_COLUMNS, Column.class);
List<String> types = child.getPropertyAsList(Property.PROJECT_COLUMN_TYPES, String.class);
if (columns != null && addIfMissing(context, sortColumn, columns, types)) {
child.setProperty(Property.PROJECT_COLUMNS, columns);
child.setProperty(Property.PROJECT_COLUMN_TYPES, types);
added = true;
}
}
}
return added;
}
/**
* Check the supplied list of columns for an existing column that matches the supplied {@link Column}, and if none is found
* add the supplied Column to the list and add an appropriate type.
*
* @param context the query context
* @param column the column that will be added if not already in the list; may not be null
* @param columns the list of columns; may not be null
* @param columnTypes the list of column types; may not be null
* @return true if the column was added (i.e., the lists were modified), or false if the lists were not modified
*/
protected boolean addIfMissing( QueryContext context,
Column column,
List<Column> columns,
List<String> columnTypes ) {
for (Column c : columns) {
if (!c.selectorName().equals(column.selectorName())) continue;
String cName = c.getPropertyName();
if (cName.equals(column.getPropertyName()) || cName.equals(column.getColumnName())) return false;
cName = c.getColumnName();
if (cName.equals(column.getPropertyName()) || cName.equals(column.getColumnName())) return false;
}
columns.add(column);
columnTypes.add(context.getTypeSystem().getDefaultType());
return true;
}
protected Column columnFor( SelectorName selector,
String property,
boolean includeSourceName ) {
String columnName = includeSourceName ? selector.getString() + "." + property : property;
return new Column(selector, property, columnName);
}
@Override
public String toString() {
return getClass().getSimpleName();
}
}