/** * Copyright (C) 2009-2013 FoundationDB, LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.foundationdb.sql.optimizer.rule; import com.foundationdb.sql.optimizer.rule.AggregateMapper.AggregateSourceFinder; import com.foundationdb.sql.optimizer.rule.AggregateMapper.AggregateSourceState; import com.foundationdb.server.error.UnsupportedSQLException; import com.foundationdb.sql.optimizer.plan.*; import com.foundationdb.sql.optimizer.plan.AggregateSource.Implementation; import com.foundationdb.sql.optimizer.plan.Sort.OrderByExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** Aggregates need to be split into a Project and the aggregation * proper, so that the project can go into a nested loop Map. */ public class AggregateSplitter extends BaseRule { private static final Logger logger = LoggerFactory.getLogger(AggregateSplitter.class); @Override protected Logger getLogger() { return logger; } @Override public void apply(PlanContext plan) { List<AggregateSourceState> sources = new AggregateSourceFinder(plan).find(); for (AggregateSourceState source : sources) { split(source.aggregateSource); } } @SuppressWarnings("unchecked") protected void split(AggregateSource source) { assert !source.isProjectSplitOff(); if (!source.hasGroupBy() && source.getAggregates().size() == 1) { AggregateFunctionExpression aggr1 = source.getAggregates().get(0); String fun = aggr1.getFunction(); if ((fun.equals("COUNT")) && (aggr1.getOperand() == null)) { TableSource singleTable = trivialCountStar(source); if (singleTable != null) { source.setImplementation(Implementation.COUNT_TABLE_STATUS); source.setTable(singleTable); } else // TODO: This is only to support the old // Count_Default operator while we have it. source.setImplementation(Implementation.COUNT_STAR); return; } else if (fun.equals("MIN") || fun.equals("MAX")) { if (directIndexMinMax(source)) { source.setImplementation(Implementation.FIRST_FROM_INDEX); // Still need project to get correct field. } } } Object[] distinctOrderby = checkDistinctDistinctsAndOrderby(source); List<ExpressionNode> fields = source.splitOffProject(); PlanNode input = source.getInput(); // order-by and distinct cannot exist together for now // so it's safe to do this: // // order by List<OrderByExpression> orderBy = (List<OrderByExpression>)distinctOrderby[1]; PlanNode ninput; if (orderBy != null) { ninput = new Sort(input, orderBy); ninput = new Project(ninput, fields); } // distinct else if ((Boolean)distinctOrderby[0]) { ninput = new Project(input, fields); ninput = new Distinct(ninput); } else ninput = new Project(input, fields); source.replaceInput(input, ninput); } /** Check that all the <code>DISTINCT</code> qualifications are * for the same expression and that there are no * non-<code>DISTINCT</code> unless they are for the same * expression and unaffected by it. */ protected Object[] checkDistinctDistinctsAndOrderby(AggregateSource source) { // boolean orderBy = false; boolean distinct = true; List<OrderByExpression> ret = null; ExpressionNode operand = null; for (AggregateFunctionExpression aggregate : source.getAggregates()) { if (aggregate.isDistinct()) { ExpressionNode other = aggregate.getOperand(); if (operand == null) operand = other; else if (!matchExpressionNode(operand, other)) throw new UnsupportedSQLException("More than one DISTINCT", other.getSQLsource()); } List<OrderByExpression> cur = aggregate.getOrderBy(); if (ret == null) ret = cur; else if (cur != null && !cur.equals(ret)) throw new UnsupportedSQLException("Mix of ORDERY-BY ", aggregate.getSQLsource()); } if (operand == null) distinct = false; else for (AggregateFunctionExpression aggregate : source.getAggregates()) { if (!aggregate.isDistinct()) { ExpressionNode other = aggregate.getOperand(); if (!matchExpressionNode(operand,other)) throw new UnsupportedSQLException("Mix of DISTINCT and non-DISTINCT", operand.getSQLsource()); else if (!distinctDoesNotMatter(aggregate.getFunction())) throw new UnsupportedSQLException("Mix of DISTINCT and non-DISTINCT", other.getSQLsource()); } } // both distinct and order-by are used, throw an error for now if (ret != null && distinct) throw new UnsupportedSQLException("Use of BOTH DISTINCT and ORDER-BY is not supported yet in" + source.getName()); return new Object[]{distinct, ret}; } protected boolean matchExpressionNode (ExpressionNode operand, ExpressionNode other) { if (operand instanceof CastExpression) { if (other instanceof CastExpression) { return ((CastExpression)operand).getOperand().equals(((CastExpression)operand).getOperand()); } return ((CastExpression)operand).getOperand().equals(other); } else if (other instanceof CastExpression) { return ((CastExpression)other).getOperand().equals(operand); } else return operand.equals(other); } protected boolean distinctDoesNotMatter(String aggregateFunction) { return ("MAX".equals(aggregateFunction) || "MIN".equals(aggregateFunction)); } /** COUNT(*) from single table? */ protected TableSource trivialCountStar(AggregateSource source) { PlanNode input = source.getInput(); if (!(input instanceof Select)) return null; Select select = (Select)input; if (!select.getConditions().isEmpty()) return null; input = select.getInput(); if (input instanceof SingleIndexScan) { SingleIndexScan index = (SingleIndexScan)input; if (index.isCovering() && !index.hasConditions() && index.getIndex().isTableIndex()) return index.getLeafMostTable(); } else if (input instanceof Flatten) { Flatten flatten = (Flatten)input; if (flatten.getTableNodes().size() != 1) return null; input = flatten.getInput(); if (input instanceof GroupScan) return flatten.getTableSources().get(0); } return null; } /** MIN(val) from index ordering by val? */ protected boolean directIndexMinMax(AggregateSource source) { PlanNode input = source.getInput(); if (!(input instanceof Select)) return false; Select select = (Select)input; if (!select.getConditions().isEmpty()) return false; input = select.getInput(); if (!(input instanceof IndexScan)) return false; IndexScan index = (IndexScan)input; int nequals = index.getNEquality(); List<Sort.OrderByExpression> ordering = index.getOrdering(); // Get number of leading columns available for ordering. This // includes those tested for equality, which only have that // one value. int ncols = (nequals < ordering.size()) ? (nequals + 1) : nequals; AggregateFunctionExpression aggr1 = source.getAggregates().get(0); for (int i = 0; i < ncols; i++) { Sort.OrderByExpression orderBy = ordering.get(i); if (orderBy.getExpression() == null) continue; if (orderBy.getExpression().equals(aggr1.getOperand())) { if ((i == nequals) && (orderBy.isAscending() != aggr1.getFunction().equals("MIN"))) { // Fetching the MAX of an ascending index (or MIN // of descending): reverse the scan to get it // first. (Order doesn't matter on the // equalities, MIN and MAX are the same.) for (Sort.OrderByExpression otherOtherBy : ordering) { otherOtherBy.setAscending(!otherOtherBy.isAscending()); } } if ((index instanceof SingleIndexScan) && (index.getOrderEffectiveness() == IndexScan.OrderEffectiveness.NONE)) { SingleIndexScan sindex = (SingleIndexScan)index; if (sindex.getConditionRange() != null) { // Need to make sure index gets the right kind of merge. sindex.setOrderEffectiveness(IndexScan.OrderEffectiveness.FOR_MIN_MAX); } } return true; } } return false; } }