/* * Copyright (C) 2010 eXo Platform SAS. * * This 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 software 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 software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xcmis.search.query.plan; import org.xcmis.search.InvalidQueryException; import org.xcmis.search.VisitException; import org.xcmis.search.Visitors; import org.xcmis.search.content.ColumnDoesNotExistOnTable; import org.xcmis.search.content.TableDoesntExistException; import org.xcmis.search.content.Schema.Table; import org.xcmis.search.model.Limit; import org.xcmis.search.model.Query; import org.xcmis.search.model.column.Column; import org.xcmis.search.model.constraint.And; import org.xcmis.search.model.constraint.Constraint; import org.xcmis.search.model.ordering.Ordering; import org.xcmis.search.model.source.Join; import org.xcmis.search.model.source.Selector; import org.xcmis.search.model.source.SelectorName; import org.xcmis.search.model.source.Source; import org.xcmis.search.query.QueryExecutionContext; import org.xcmis.search.query.plan.QueryExecutionPlan.JoinExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.LimitExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.ProjectExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.SelectorExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.SortExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.SourceExecutionPlan; import org.xcmis.search.query.plan.QueryExecutionPlan.WhereExecutionPlan; import org.xcmis.search.query.validate.Validator; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Stack; /** * The planner that produces a simple query plan given a {@link Query query }. * <p> * A simple plan always has the same structure: * * <pre> * LIMIT if row limit or offset are used * | * SORTING if 'ORDER BY' is used with more then one Source * | * WHERE1 * | One or more WHERE plan nodes that each have * WHERE2 a single non-join constraint * | * WHEREn * | * SOURCE or JOIN A single SOURCE or JOIN node, depending upon the query * / \ * / \ * SOJ SOJ A SOURCE or JOIN node for the left and right side of the JOIN * </pre> * <p> * There leaves of the tree are always SOURCE nodes, so <i>conceptually</i> data always flows through this plan from the bottom * SOURCE nodes, is adjusted/filtered as it trickles up through the plan, and is then ready to be used by the caller as it emerges * from the top node of the plan. * </p> * <p> * This canonical plan, however, is later optimized and rearranged so that it performs faster. * </p> */ public class SimplePlaner implements QueryExecutionPlaner { /** * @see org.xcmis.search.query.plan.QueryExecutionPlaner#createPlan(org.xcmis.search.query.QueryExecutionContext, org.xcmis.search.model.Query) */ public QueryExecutionPlan createPlan(QueryExecutionContext context, Query query) { try { Map<SelectorName, Table> querySelectorsMap = new HashMap<SelectorName, Table>(); //source QueryExecutionPlan plan = createSelectorPlan(context, query.getSource(), querySelectorsMap); //constrain plan = createConstrainPlan(context, query.getConstraint(), querySelectorsMap, plan); //columns plan = createProject(context, query.getColumns(), querySelectorsMap, plan); //order by plan = createSorting(context, query.getOrderings(), plan); //limit plan = createLimits(context, query.getLimits(), plan); Visitors.visitAll(query, new Validator(context, querySelectorsMap)); return plan; } catch (VisitException e) { context.getExecutionExceptions() .addException(new InvalidQueryException(e.getLocalizedMessage(), e.getCause())); } return null; } /** * @param context * @param constraint * @param querySelectorsMap * @param executionPlan * @return */ protected QueryExecutionPlan createConstrainPlan(final QueryExecutionContext context, final Constraint constraint, final Map<SelectorName, Table> querySelectorsMap, QueryExecutionPlan executionPlan) { // Extract the list of Constraint objects that all must be satisfied ... LinkedList<Constraint> andableConstraints = new LinkedList<Constraint>(); separateAndConstraints(constraint, andableConstraints); // For each of these constraints, create a criteria (WHERE) node above the supplied (JOIN or SOURCE) node. // Do this in reverse order so that the top-most WHERE node corresponds to the first constraint. for (Constraint andedConstrain : andableConstraints) { // Create the where step ... WhereExecutionPlan whereExecutionPlan = new WhereExecutionPlan(executionPlan); whereExecutionPlan.setConstraint(andedConstrain); // Add selectors to the criteria node ... whereExecutionPlan.addSelectors(Visitors.getSelectorsReferencedBy(andedConstrain)); executionPlan = whereExecutionPlan; } return executionPlan; } /** * Populate plan for given selector. * @param context * @param source * @param querySelectorsMap * @return QueryExecutionPlan * @throws VisitException */ protected QueryExecutionPlan createSelectorPlan(final QueryExecutionContext context, final Source source, final Map<SelectorName, Table> querySelectorsMap) throws VisitException { final Stack<QueryExecutionPlan> stepsStack = new Stack<QueryExecutionPlan>(); Visitors.visit(source, new Visitors.AbstractModelVisitor() { /** * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.source.Join) */ @Override public void visit(Join node) throws VisitException { JoinExecutionPlan joinPlan = new JoinExecutionPlan(); joinPlan.setJoinType(node.getType()); joinPlan.setJoinAlgorithm(JoinAlgorithm.NESTED_LOOP); joinPlan.setJoinCondition(node.getJoinCondition()); //left plan node.getLeft().accept(this); joinPlan.setLeftPlan((SourceExecutionPlan)stepsStack.pop()); //right plan node.getRight().accept(this); joinPlan.setRightPlan((SourceExecutionPlan)stepsStack.pop()); } /** * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.source.Selector) */ @Override public void visit(Selector selector) { //QueryExecutionPlan executionPlan = new QueryExecutionPlan(QueryExecutionPlan.Type.SELECTOR); SelectorExecutionPlan selectorPlan = new SelectorExecutionPlan(); if (selector.hasAlias()) { selectorPlan.addSelector(selector.getAlias()); selectorPlan.setAlias(selector.getAlias()); selectorPlan.setName(selector.getName()); } else { selectorPlan.addSelector(selector.getName()); selectorPlan.setName(selector.getName()); } // Validate the source name and set the available columns ... Table table = context.getSchema().getTable(selector.getName()); if (table != null) { if (querySelectorsMap.put(selector.getAliasOrName(), table) != null) { // There was already a table with this alias or name ... } selectorPlan.setColumns(table.getColumns()); } else { context.getExecutionExceptions().addException( new TableDoesntExistException("Table " + selector.getName() + " doesn't exist")); } stepsStack.push(selectorPlan); } }); return stepsStack.pop(); } /** * populate SORT node at top of executionPlan. The SORT may be pushed down to a source (or sources) if possible by the optimizer. * * @param context the context in which the query is being planned * @param orderings list of orderings from the query * @param executionPlan the existing plan */ protected QueryExecutionPlan createSorting(final QueryExecutionContext context, List<Ordering> orderings, final QueryExecutionPlan executionPlan) { if (!orderings.isEmpty()) { SortExecutionPlan sortExecutionPlan = new SortExecutionPlan(executionPlan); sortExecutionPlan.setOrderings(orderings); for (Ordering ordering : orderings) { sortExecutionPlan.addSelectors(Visitors.getSelectorsReferencedBy(ordering)); } return sortExecutionPlan; } return executionPlan; } /** * Attach a PROJECT node at the top of the plan tree. * * @param context the context in which the query is being planned * @param executionPlan the existing plan * @param columns the columns being projected; may be null * @param selectors the selectors keyed by their alias or name * @return the updated plan */ protected QueryExecutionPlan createProject(final QueryExecutionContext context, List<Column> columns, Map<SelectorName, Table> selectors, QueryExecutionPlan executionPlan) { if (columns == null) { columns = Collections.emptyList(); } //QueryExecutionPlan projectNode = new QueryExecutionPlan(QueryExecutionPlan.Type.PROJECT); ProjectExecutionPlan projectPlan = new ProjectExecutionPlan(executionPlan); if (columns.isEmpty() || (columns.size() == 1 && columns.get(0).getPropertyName().equals("*"))) { columns = new LinkedList<Column>(); // SELECT *, so find all of the columns that are available from all the sources ... for (Map.Entry<SelectorName, Table> entry : selectors.entrySet()) { SelectorName tableName = entry.getKey(); Table table = entry.getValue(); // Add the selector that is being used ... projectPlan.addSelector(tableName); // Compute the columns from this selector ... for (org.xcmis.search.content.Schema.Column column : table.getColumns()) { String columnName = column.getName(); String propertyName = columnName; columns.add(new Column(tableName, propertyName, columnName)); } } } else { // Add the selector used by each column ... for (Column column : columns) { if (!column.isFunction()) { SelectorName tableName = column.getSelectorName(); // Add the selector that is being used ... projectPlan.addSelector(tableName); // Verify that each column is available in the appropriate source ... Table table = selectors.get(tableName); if (table == null) { context.getExecutionExceptions().addException( new TableDoesntExistException("Table " + tableName + " doesn't exist")); } else { // Make sure that the column is in the table ... String name = column.getPropertyName(); if (table.getColumn(name) == null) { context.getExecutionExceptions().addException( new ColumnDoesNotExistOnTable("Column " + name + " on " + tableName + " doesn't exist")); } } } } } projectPlan.setColumns(columns); return projectPlan; } /** * Attach a LIMIT node at the top of the plan tree. * * @param context the context in which the query is being planned * @param limit the limit definition; may be null * @param executionPlan the existing plan * */ protected QueryExecutionPlan createLimits(QueryExecutionContext context, Limit limit, QueryExecutionPlan executionPlan) { if (limit != null && !limit.isUnlimited()) { LimitExecutionPlan limitExecutionPlan = new LimitExecutionPlan(executionPlan); limitExecutionPlan.setLimit(limit); return limitExecutionPlan; } return executionPlan; } /** * Walk the supplied constraint to extract a list of the constraints that can be AND-ed together. For example, given the * constraint tree ((C1 AND C2) AND (C3 OR C4)), this method would result in a list of three separate criteria: [C1,C2,(C3 OR * C4)]. The resulting <code>andConstraints</code> list will contain Constraint objects that all must be true. * * @param constraint the input constraint * @param andableConstraints the collection into which all non-{@link And AND} constraints should be placed */ protected void separateAndConstraints(Constraint constraint, List<Constraint> andableConstraints) { if (constraint == null) { return; } if (constraint instanceof And) { And and = (And)constraint; separateAndConstraints(and.getLeft(), andableConstraints); separateAndConstraints(and.getRight(), andableConstraints); } else { andableConstraints.add(constraint); } } }