/**
* 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.join_enum;
import com.foundationdb.server.error.CorruptedPlanException;
import com.foundationdb.server.spatial.Spatial;
import com.foundationdb.sql.optimizer.rule.EquivalenceFinder;
import com.foundationdb.sql.optimizer.rule.JoinAndIndexPicker;
import com.foundationdb.sql.optimizer.rule.PlanContext;
import com.foundationdb.sql.optimizer.rule.SchemaRulesContext;
import com.foundationdb.sql.optimizer.rule.cost.CostEstimator.SelectivityConditions;
import com.foundationdb.sql.optimizer.rule.cost.PlanCostEstimator;
import com.foundationdb.sql.optimizer.rule.join_enum.DPhyp.JoinOperator;
import com.foundationdb.sql.optimizer.rule.range.ColumnRanges;
import com.foundationdb.sql.optimizer.plan.*;
import com.foundationdb.sql.optimizer.plan.Sort.OrderByExpression;
import com.foundationdb.sql.optimizer.plan.TableGroupJoinTree.TableGroupJoinNode;
import com.foundationdb.ais.model.*;
import com.foundationdb.ais.model.Index.JoinType;
import com.foundationdb.server.error.UnsupportedSQLException;
import com.foundationdb.server.service.text.FullTextQueryBuilder;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.common.funcs.GeoOverlaps;
import com.foundationdb.server.types.texpressions.Comparison;
import com.foundationdb.sql.types.DataTypeDescriptor;
import com.foundationdb.sql.types.TypeId;
import com.google.common.base.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/** The goal of a indexes within a group. */
public class GroupIndexGoal implements Comparator<BaseScan>
{
private static final Logger logger = LoggerFactory.getLogger(GroupIndexGoal.class);
static volatile Function<? super IndexScan,Void> intersectionEnumerationHook = null;
// The overall goal.
private QueryIndexGoal queryGoal;
// The grouped tables.
private TableGroupJoinTree tables;
// All the conditions that might be indexable.
private List<ConditionExpression> conditions;
// Where they came from.
private List<ConditionList> conditionSources;
// The set of conditions that must be applied to this scan
// (i.e. if the index doesn't cover them, a select will be required)
// These are a subset of conditions
private List<ConditionExpression> requiredConditions;
// All the columns besides those in conditions that will be needed.
private RequiredColumns requiredColumns;
// Tables already bound outside.
private Set<ColumnSource> boundTables;
// Can an index be used to take care of sorting.
// TODO: Make this a subset of queryGoal's sorting, based on what
// tables are in what join, rather than an all or nothing.
private boolean sortAllowed;
// Mapping of Range-expressible conditions, by their column. lazy loaded.
private Map<ColumnExpression,ColumnRanges> columnsToRanges;
private PlanContext queryContext;
public GroupIndexGoal(QueryIndexGoal queryGoal, TableGroupJoinTree tables, PlanContext queryContext) {
this.queryGoal = queryGoal;
this.tables = tables;
this.queryContext = queryContext;
if (queryGoal.getWhereConditions() != null) {
conditionSources = Collections.singletonList(queryGoal.getWhereConditions());
conditions = queryGoal.getWhereConditions();
}
else {
conditionSources = Collections.emptyList();
conditions = Collections.emptyList();
}
requiredConditions = conditions;
requiredColumns = new RequiredColumns(tables);
boundTables = queryGoal.getQuery().getOuterTables();
sortAllowed = true;
}
public QueryIndexGoal getQueryGoal() {
return queryGoal;
}
public TableGroupJoinTree getTables() {
return tables;
}
public Collection<? extends ColumnSource> getTableColumnSources() {
List<TableSource> result = new ArrayList<>();
for (TableGroupJoinNode table : tables) {
result.add(table.getTable());
}
return result;
}
/**
* @param boundTables Tables already bound by the outside. Columns of these tables
* are like constants for the sake of this index selection.
* @param queryJoins Joins that come from the query, or part of the query,
* that an index is being searched for.
* The type of these joins determines whether a column in a
* condition is nullable.
* Will generally match <code>joins</code>, except in the case
* of a subquery with only one table group, where it is empty,
* because it is the derived table that is nullable, not the inside
* of the subquery.
* @param joins Joins that apply to this part of the query. The conditions of these
* are ones that might be indexed.
* @param outsideJoins All joins for this query. A column used in one of these must
* be supplied by its index source in determining whether that
* is covering.
* @param requiredJoins The joins that must be performed by the resulting join,
* either in the index scan conditions or an outer
* <code>Select</code> node, whose cost is then added.
* @param sortAllowed <code>true</code> if sorting from this index selection might
* accomplish sorting for the whole query.
* @param extraConditions Conditions that can be indexed but were invented solely for
* this index selection. For instance, a semi-join to
* <code>VALUES</code> appears here as equality with a column.
* @return Full list of all usable condition sources. If this index is chosen, its
* conditions should each come, and be removed from, from one of these.
*/
public List<ConditionList> updateContext(Set<ColumnSource> boundTables,
Collection<JoinOperator> queryJoins,
Collection<JoinOperator> joins,
Collection<JoinOperator> outsideJoins,
Collection<JoinOperator> requiredJoins,
boolean sortAllowed,
ConditionList extraConditions) {
setBoundTables(boundTables);
this.sortAllowed = sortAllowed;
setJoinConditions(queryJoins, joins, requiredJoins, extraConditions);
updateRequiredColumns(joins, outsideJoins);
return conditionSources;
}
public void setBoundTables(Set<ColumnSource> boundTables) {
this.boundTables = boundTables;
}
private static boolean hasOuterJoin(Collection<JoinOperator> joins) {
for (JoinOperator joinOp : joins) {
switch (joinOp.getJoinType()) {
case LEFT:
case RIGHT:
case FULL_OUTER:
return true;
}
}
return false;
}
public void setJoinConditions(Collection<JoinOperator> queryJoins, Collection<JoinOperator> joins,
Collection<JoinOperator> requiredJoins, ConditionList extraConditions) {
conditionSources = new ArrayList<>();
boolean hasOuterJoin = hasOuterJoin(queryJoins);
if ((queryGoal.getWhereConditions() != null) && !hasOuterJoin) {
conditionSources.add(queryGoal.getWhereConditions());
}
for (JoinOperator join : joins) {
ConditionList joinConditions = join.getJoinConditions();
if (joinConditions != null)
conditionSources.add(joinConditions);
}
if (extraConditions != null) {
conditionSources.add(extraConditions);
}
switch (conditionSources.size()) {
case 0:
conditions = Collections.emptyList();
break;
case 1:
conditions = conditionSources.get(0);
break;
default:
conditions = new ArrayList<>();
for (ConditionList conditionSource : conditionSources) {
conditions.addAll(conditionSource);
}
}
buildRequiredConditions(requiredJoins, queryGoal.getWhereConditions(), hasOuterJoin);
columnsToRanges = null;
}
private void buildRequiredConditions(Collection<JoinOperator> requiredJoins,
ConditionList whereConditions, boolean hasOuterJoin) {
requiredConditions = new ArrayList<>();
if (requiredJoins != null) {
for (JoinOperator join : requiredJoins) {
ConditionList joinConditions = join.getJoinConditions();
if (joinConditions != null)
requiredConditions.addAll(joinConditions);
}
}
if (whereConditions != null && !hasOuterJoin) {
for (ConditionExpression condition : whereConditions) {
requiredConditions.add(condition);
}
}
}
public void updateRequiredColumns(Collection<JoinOperator> joins,
Collection<JoinOperator> outsideJoins) {
requiredColumns.clear();
Collection<PlanNode> orderings = (queryGoal.getOrdering() == null) ?
Collections.<PlanNode>emptyList() :
Collections.<PlanNode>singletonList(queryGoal.getOrdering());
RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredColumns,
orderings, conditions);
queryGoal.getQuery().accept(filler);
for (JoinOperator join : outsideJoins) {
if (joins.contains(join)) continue;
ConditionList joinConditions = join.getJoinConditions();
if (joinConditions != null) {
for (ConditionExpression condition : joinConditions) {
condition.accept(filler);
}
}
}
}
/** Given a semi-join to a VALUES, see whether it can be turned
* into a predicate on some column in this group, in which case it
* can possibly be indexed.
*/
public InListCondition semiJoinToInList(ExpressionsSource values,
Collection<JoinOperator> joins) {
if (values.nFields() != 1)
return null;
ComparisonCondition ccond = null;
boolean found = false;
ConditionExpression joinCondition = onlyJoinCondition(joins);
if (joinCondition instanceof ComparisonCondition) {
ccond = (ComparisonCondition)joinCondition;
if ((ccond.getOperation() == Comparison.EQ) &&
(ccond.getRight() instanceof ColumnExpression)) {
ColumnExpression rcol = (ColumnExpression)ccond.getRight();
if ((rcol.getTable() == values) &&
(rcol.getPosition() == 0) &&
(ccond.getLeft() instanceof ColumnExpression)) {
ColumnExpression lcol = (ColumnExpression)ccond.getLeft();
for (TableGroupJoinNode table : tables) {
if (table.getTable() == lcol.getTable()) {
found = true;
break;
}
}
}
}
}
if (!found) return null;
return semiJoinToInList(values, ccond, queryGoal.getRulesContext());
}
protected static ConditionExpression onlyJoinCondition(Collection<JoinOperator> joins) {
ConditionExpression result = null;
for (JoinOperator join : joins) {
if (join.getJoinConditions() != null) {
for (ConditionExpression cond : join.getJoinConditions()) {
if (result == null)
result = cond;
else
return null;
}
}
}
return result;
}
public static InListCondition semiJoinToInList(ExpressionsSource values,
ComparisonCondition ccond,
SchemaRulesContext rulesContext) {
List<ExpressionNode> expressions = new ArrayList<>(values.getExpressions().size());
for (List<ExpressionNode> row : values.getExpressions()) {
expressions.add(row.get(0));
}
DataTypeDescriptor sqlType = new DataTypeDescriptor(TypeId.BOOLEAN_ID, true);
TInstance type = rulesContext.getTypesTranslator().typeForSQLType(sqlType);
InListCondition cond = new InListCondition(ccond.getLeft(), expressions,
sqlType, null, type);
cond.setComparison(ccond);
//cond.setPreptimeValue(new TPreptimeValue(AkBool.INSTANCE.instance(true)));
return cond;
}
/** Populate given index usage according to goal.
* @return <code>false</code> if the index is useless.
*/
public boolean usable(SingleIndexScan index) {
setColumnsAndOrdering(index);
int nequals = insertLeadingEqualities(index, conditions);
if (index.getIndex().isSpatial()) return spatialUsable(index, nequals);
List<ExpressionNode> indexExpressions = index.getColumns();
if (nequals < indexExpressions.size()) {
ExpressionNode indexExpression = indexExpressions.get(nequals);
if (indexExpression != null) {
boolean foundInequalityCondition = false;
for (ConditionExpression condition : conditions) {
if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
if (ccond.getOperation() == Comparison.NE)
continue; // ranges are better suited for !=
ExpressionNode otherComparand = matchingComparand(indexExpression, ccond);
if (otherComparand != null) {
Comparison op = ccond.getOperation();
if (otherComparand == ccond.getLeft())
op = ComparisonCondition.reverseComparison(op);
index.addInequalityCondition(condition, op, otherComparand);
foundInequalityCondition = true;
}
}
}
if (!foundInequalityCondition) {
ColumnRanges range = rangeForIndex(indexExpression);
if (range != null)
index.addRangeCondition(range);
}
}
}
index.setOrderEffectiveness(determineOrderEffectiveness(index));
index.setCovering(determineCovering(index));
if ((index.getOrderEffectiveness() == IndexScan.OrderEffectiveness.NONE) &&
!index.hasConditions() &&
!index.isCovering())
return false;
index.setCostEstimate(estimateCost(index));
return true;
}
private int insertLeadingEqualities(EqualityColumnsScan index, List<ConditionExpression> localConds) {
int nequals = 0;
List<ExpressionNode> indexExpressions = index.getColumns();
int ncols = indexExpressions.size();
while (nequals < ncols) {
ExpressionNode indexExpression = indexExpressions.get(nequals);
if (indexExpression == null) break;
ConditionExpression equalityCondition = null;
ExpressionNode otherComparand = null;
for (ConditionExpression condition : localConds) {
if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
if (ccond.getOperation() != Comparison.EQ)
continue; // only doing equalities
ExpressionNode comparand = matchingComparand(indexExpression, ccond);
if (comparand != null) {
equalityCondition = condition;
otherComparand = comparand;
break;
}
}
else if (condition instanceof FunctionCondition) {
FunctionCondition fcond = (FunctionCondition)condition;
if (fcond.getFunction().equals("isNull") &&
(fcond.getOperands().size() == 1) &&
indexExpressionMatches(indexExpression,
fcond.getOperands().get(0))) {
ExpressionNode foperand = fcond.getOperands().get(0);
equalityCondition = condition;
otherComparand = new IsNullIndexKey(foperand.getSQLtype(),
fcond.getSQLsource(),
foperand.getType());
break;
}
}
}
if (equalityCondition == null)
break;
index.addEqualityCondition(equalityCondition, otherComparand);
nequals++;
}
return nequals;
}
private ExpressionNode matchingComparand(ExpressionNode indexExpression,
ComparisonCondition ccond) {
ExpressionNode comparand;
if (indexExpressionMatches(indexExpression, ccond.getLeft())) {
comparand = ccond.getRight();
if (constantOrBound(comparand))
return comparand;
}
if (indexExpressionMatches(indexExpression, ccond.getRight())) {
comparand = ccond.getLeft();
if (constantOrBound(comparand))
return comparand;
}
return null;
}
private static void setColumnsAndOrdering(SingleIndexScan index) {
Index aisIndex = index.getIndex();
List<IndexColumn> indexColumns = aisIndex.getAllColumns();
int ncols = indexColumns.size();
int firstSpatialColumn, spatialColumns;
SpecialIndexExpression.Function spatialFunction;
Index.IndexMethod aisIndexMethod = aisIndex.getIndexMethod();
switch (aisIndexMethod) {
case GEO_LAT_LON:
case GEO_WKB:
case GEO_WKT:
firstSpatialColumn = aisIndex.firstSpatialArgument();
spatialColumns = aisIndex.spatialColumns();
if (spatialColumns == Spatial.LAT_LON_DIMENSIONS) {
assert aisIndexMethod == Index.IndexMethod.GEO_LAT_LON : aisIndexMethod;
spatialFunction = SpecialIndexExpression.Function.GEO_LAT_LON;
}
else if (spatialColumns == 1) {
if (aisIndexMethod == Index.IndexMethod.GEO_WKB) {
spatialFunction = SpecialIndexExpression.Function.GEO_WKB;
}
else if (aisIndexMethod == Index.IndexMethod.GEO_WKT) {
spatialFunction = SpecialIndexExpression.Function.GEO_WKT;
}
else {
spatialFunction = null;
assert false : aisIndexMethod;
}
}
else {
spatialFunction = null;
assert false : spatialColumns;
}
break;
default:
assert false : aisIndexMethod;
/* and fall through */
case NORMAL:
firstSpatialColumn = Integer.MAX_VALUE;
spatialColumns = 0;
spatialFunction = null;
break;
}
List<OrderByExpression> orderBy = new ArrayList<>(ncols);
List<ExpressionNode> indexExpressions = new ArrayList<>(ncols);
int i = 0;
while (i < ncols) {
ExpressionNode indexExpression;
boolean ascending;
if (i == firstSpatialColumn) {
List<ExpressionNode> operands = new ArrayList<>(spatialColumns);
for (int j = 0; j < spatialColumns; j++) {
operands.add(getIndexExpression(index, indexColumns.get(i++)));
}
indexExpression = new SpecialIndexExpression(spatialFunction, operands);
ascending = true;
}
else {
IndexColumn indexColumn = indexColumns.get(i++);
indexExpression = getIndexExpression(index, indexColumn);
ascending = indexColumn.isAscending();
}
indexExpressions.add(indexExpression);
orderBy.add(new OrderByExpression(indexExpression, ascending));
}
index.setColumns(indexExpressions);
index.setOrdering(orderBy);
}
// Take ordering from output index and adjust ordering of others to match.
private static void installOrdering(IndexScan index,
List<OrderByExpression> outputOrdering,
int outputPeggedCount, int comparisonFields) {
if (index instanceof SingleIndexScan) {
List<OrderByExpression> indexOrdering = index.getOrdering();
if ((indexOrdering != null) && (indexOrdering != outputOrdering)) {
// Order comparison fields the same way as output.
// Try to avoid mixed mode: initial columns ordered
// like first comparison, trailing columns ordered
// like last comparison.
int i = 0;
boolean ascending = outputOrdering.get(outputPeggedCount).isAscending();
while (i < index.getPeggedCount()) {
indexOrdering.get(i++).setAscending(ascending);
}
for (int j = 0; j < comparisonFields; j++) {
assert outputOrdering.get(outputPeggedCount + j).isAscending() == ascending : "comparison ascending different order than initial";
indexOrdering.get(i++).setAscending(outputOrdering.get(outputPeggedCount + j).isAscending());
}
boolean lastAscending = outputOrdering.get(outputPeggedCount + comparisonFields - 1).isAscending();
assert ascending == lastAscending : "lastAscending in different order than initial";
while (i < indexOrdering.size()) {
indexOrdering.get(i++).setAscending(lastAscending);
}
}
}
else if (index instanceof MultiIndexIntersectScan) {
MultiIndexIntersectScan multiIndex = (MultiIndexIntersectScan)index;
installOrdering(multiIndex.getOutputIndexScan(),
outputOrdering, outputPeggedCount, comparisonFields);
installOrdering(multiIndex.getSelectorIndexScan(),
outputOrdering, outputPeggedCount, comparisonFields);
}
}
private ExpressionNode targetExpression(OrderByExpression targetColumn) {
ExpressionNode targetExpression = targetColumn.getExpression();
if (targetExpression.isColumn()) {
ColumnExpression column = (ColumnExpression)targetExpression;
ColumnSource table = column.getTable();
if (table == queryGoal.getGrouping()) {
targetExpression = queryGoal.getGrouping()
.getField(column.getPosition());
}
else if (table instanceof Project) {
// Cf. ASTStatementLoader.sortsForDistinct().
Project project = (Project)table;
if ((project.getOutput() == queryGoal.getOrdering()) &&
(queryGoal.getOrdering().getOutput() instanceof Distinct)) {
targetExpression = project.getFields()
.get(column.getPosition());
}
}
}
return targetExpression;
}
// Determine how well this index does against the target.
// Also, correct traversal order to match sort if possible.
protected IndexScan.OrderEffectiveness determineOrderEffectiveness(SingleIndexScan index) {
IndexScan.OrderEffectiveness result = IndexScan.OrderEffectiveness.NONE;
if (!sortAllowed) return result;
List<OrderByExpression> indexOrdering = index.getOrdering();
if (indexOrdering == null) return result;
int nequals = index.getNEquality();
int nunions = index.getNUnions();
List<ExpressionNode> equalityColumns = null;
if (nequals - nunions > 0) {
equalityColumns = index.getColumns().subList(0, nequals - nunions);
}
try_sorted:
if (queryGoal.getOrdering() != null) {
int idx = nequals-nunions;
for (OrderByExpression targetColumn : queryGoal.getOrdering().getOrderBy()) {
// Get the expression by which this is ordering, recognizing the
// special cases where the Sort is fed by GROUP BY or feeds DISTINCT.
ExpressionNode targetExpression = targetExpression(targetColumn);
OrderByExpression indexColumn = getIndexColumn(indexOrdering, idx);
if (indexColumn == null && idx < nequals) {
throw new CorruptedPlanException("No index column expression for union comparison");
}
if (indexColumn != null) {
boolean matchingColumn = orderingExpressionMatches(indexColumn, targetExpression);
if (!matchingColumn && idx < nequals) {
// if we we're trying the union column, but that failed, try just treating it as equals
idx++;
indexColumn = getIndexColumn(indexOrdering, idx);
if (indexColumn != null) {
matchingColumn = orderingExpressionMatches(indexColumn, targetExpression);
if (matchingColumn) {
index.setIncludeUnionAsEquality(true);
}
}
}
if (matchingColumn) {
if (idx < nequals) {
index.setIncludeUnionAsEquality(false);
}
if (idx >= index.getNKeyColumns())
index.setUsesAllColumns(true);
idx++;
continue;
}
}
if (equalityColumns != null) {
// Another possibility is that target ordering is
// in fact unchanged due to equality condition.
// TODO: Should this have been noticed earlier on
// so that it can be taken out of the sort?
if (equalityColumns.contains(targetExpression))
continue;
}
break try_sorted;
}
// Don't allow mixed order index lookups, just use the order of the first column
boolean isAscending = queryGoal.getOrdering().getOrderBy().get(0).isAscending();
for (OrderByExpression indexColumn : indexOrdering) {
if (indexColumn.isAscending() != isAscending) {
indexColumn.setAscending(isAscending);
}
}
boolean orderByOrdering = true;
for (OrderByExpression targetColumn : queryGoal.getOrdering().getOrderBy()) {
if (targetColumn.isAscending() != isAscending) {
orderByOrdering = false;
}
}
// If the order by ordering columns all matches the
// order of the index (ASC or DESC), the index is sorted.
if (orderByOrdering) {
result = IndexScan.OrderEffectiveness.SORTED;
}
}
if (queryGoal.getGrouping() != null) {
boolean anyFound = false, allFound = true;
List<ExpressionNode> groupBy = queryGoal.getGrouping().getGroupBy();
for (ExpressionNode targetExpression : groupBy) {
int found = -1;
for (int i = nequals; i < indexOrdering.size(); i++) {
if (orderingExpressionMatches(indexOrdering.get(i), targetExpression)) {
found = i - nequals;
break;
}
}
if (found < 0) {
allFound = false;
if ((equalityColumns == null) ||
!equalityColumns.contains(targetExpression))
continue;
}
else if (found >= groupBy.size()) {
// Ordered by this column, but after some other
// stuff which will break up the group. Only
// partially grouped.
allFound = false;
}
if (found >= index.getNKeyColumns()) {
index.setUsesAllColumns(true);
}
anyFound = true;
}
if (anyFound) {
if (!allFound)
return IndexScan.OrderEffectiveness.PARTIAL_GROUPED;
else if (result == IndexScan.OrderEffectiveness.SORTED)
return result;
else
return IndexScan.OrderEffectiveness.GROUPED;
}
}
else if (queryGoal.getProjectDistinct() != null) {
assert (queryGoal.getOrdering() == null);
if (orderedForDistinct(index, queryGoal.getProjectDistinct(),
indexOrdering, nequals)) {
return IndexScan.OrderEffectiveness.SORTED;
}
}
return result;
}
private OrderByExpression getIndexColumn(List<OrderByExpression> indexOrdering, int idx) {
OrderByExpression indexColumn = null;
if (idx < indexOrdering.size()) {
indexColumn = indexOrdering.get(idx);
if (indexColumn.getExpression() == null)
indexColumn = null; // Index sorts by unknown column.
}
return indexColumn;
}
/** For use with a Distinct that gets added later. */
public boolean orderedForDistinct(Project projectDistinct, IndexScan index) {
List<OrderByExpression> indexOrdering = index.getOrdering();
if (indexOrdering == null) return false;
int nequals = index.getNEquality();
return orderedForDistinct(index, projectDistinct, indexOrdering, nequals);
}
protected boolean orderedForDistinct(IndexScan index, Project projectDistinct,
List<OrderByExpression> indexOrdering,
int nequals) {
List<ExpressionNode> distinct = projectDistinct.getFields();
for (ExpressionNode targetExpression : distinct) {
int found = -1;
for (int i = nequals; i < indexOrdering.size(); i++) {
if (orderingExpressionMatches(indexOrdering.get(i), targetExpression)) {
found = i - nequals;
break;
}
}
if ((found < 0) || (found >= distinct.size())) {
return false;
}
if (found >= index.getNKeyColumns()) {
index.setUsesAllColumns(true);
}
}
return true;
}
// Does the column expression coming from the index match the ORDER BY target,
// allowing for column equivalences?
protected boolean orderingExpressionMatches(OrderByExpression orderByExpression,
ExpressionNode targetExpression) {
ExpressionNode columnExpression = orderByExpression.getExpression();
if (columnExpression == null)
return false;
if (columnExpression.equals(targetExpression))
return true;
if (!(columnExpression instanceof ColumnExpression) ||
!(targetExpression instanceof ColumnExpression))
return false;
return getColumnEquivalencies().areEquivalent((ColumnExpression)columnExpression,
(ColumnExpression)targetExpression);
}
protected EquivalenceFinder<ColumnExpression> getColumnEquivalencies() {
return queryGoal.getQuery().getColumnEquivalencies();
}
protected class UnboundFinder implements ExpressionVisitor {
boolean found = false;
public UnboundFinder() {
}
@Override
public boolean visitEnter(ExpressionNode n) {
return visit(n);
}
@Override
public boolean visitLeave(ExpressionNode n) {
return !found;
}
@Override
public boolean visit(ExpressionNode n) {
if (n instanceof ColumnExpression) {
ColumnExpression columnExpression = (ColumnExpression)n;
if (!boundTables.contains(columnExpression.getTable())) {
found = true;
return false;
}
}
else if (n instanceof SubqueryExpression) {
for (ColumnSource used : ((SubqueryExpression)n).getSubquery().getOuterTables()) {
// Tables defined inside the subquery are okay, but ones from outside
// need to be bound to eval as an expression.
if (!boundTables.contains(used)) {
found = true;
return false;
}
}
}
return true;
}
}
/** Does the given expression have references to tables that aren't bound? */
protected boolean constantOrBound(ExpressionNode expression) {
UnboundFinder f = new UnboundFinder();
expression.accept(f);
return !f.found;
}
/** Get an expression form of the given index column. */
protected static ExpressionNode getIndexExpression(IndexScan index,
IndexColumn indexColumn) {
return getColumnExpression(index.getLeafMostTable(), indexColumn.getColumn());
}
/** Get an expression form of the given group column. */
protected static ExpressionNode getColumnExpression(TableSource leafMostTable,
Column column) {
Table indexTable = column.getTable();
for (TableSource table = leafMostTable;
null != table;
table = table.getParentTable()) {
if (table.getTable().getTable() == indexTable) {
return new ColumnExpression(table, column);
}
}
return null;
}
/** Is the comparison operand what the index indexes? */
protected boolean indexExpressionMatches(ExpressionNode indexExpression,
ExpressionNode comparisonOperand) {
if (indexExpression.equals(comparisonOperand))
return true;
if (!(indexExpression instanceof ColumnExpression) ||
!(comparisonOperand instanceof ColumnExpression))
return false;
if (getColumnEquivalencies().areEquivalent((ColumnExpression)indexExpression,
(ColumnExpression)comparisonOperand))
return true;
// See if comparing against a result column of the subquery,
// that is, a join to the subquery that we can push down.
ColumnExpression comparisonColumn = (ColumnExpression)comparisonOperand;
ColumnSource comparisonTable = comparisonColumn.getTable();
if (!(comparisonTable instanceof SubquerySource))
return false;
Subquery subquery = ((SubquerySource)comparisonTable).getSubquery();
if (subquery != queryGoal.getQuery())
return false;
PlanNode input = subquery.getQuery();
if (input instanceof ResultSet)
input = ((ResultSet)input).getInput();
if (!(input instanceof Project))
return false;
Project project = (Project)input;
ExpressionNode insideExpression = project.getFields().get(comparisonColumn.getPosition());
return indexExpressionMatches(indexExpression, insideExpression);
}
/** Find the best index among the branches. */
public BaseScan pickBestScan() {
logger.debug("Picking for {}", this);
BaseScan bestScan = null;
bestScan = pickFullText();
if (bestScan != null) {
return bestScan; // TODO: Always wins for now.
}
if (tables.getGroup().getRejectedJoins() != null) {
bestScan = pickBestGroupLoop();
}
IntersectionEnumerator intersections = new IntersectionEnumerator();
Set<TableSource> required = tables.getRequired();
for (TableGroupJoinNode table : tables) {
IndexScan tableIndex = pickBestIndex(table, required, intersections);
if ((tableIndex != null) &&
((bestScan == null) || (compare(tableIndex, bestScan) > 0)))
bestScan = tableIndex;
ExpressionsHKeyScan hKeyRow = pickHKeyRow(table, required);
if ((hKeyRow != null) &&
((bestScan == null) || (compare(hKeyRow, bestScan) > 0)))
bestScan = hKeyRow;
}
bestScan = pickBestIntersection(bestScan, intersections);
if (bestScan == null) {
GroupScan groupScan = new GroupScan(tables.getGroup());
groupScan.setCostEstimate(estimateCost(groupScan));
bestScan = groupScan;
}
return bestScan;
}
private BaseScan pickBestIntersection(BaseScan previousBest, IntersectionEnumerator enumerator) {
// filter out all leaves which are obviously bad
if (previousBest != null) {
CostEstimate previousBestCost = previousBest.getCostEstimate();
for (Iterator<SingleIndexScan> iter = enumerator.leavesIterator(); iter.hasNext(); ) {
SingleIndexScan scan = iter.next();
CostEstimate scanCost = estimateIntersectionCost(scan);
if (scanCost.compareTo(previousBestCost) > 0) {
logger.debug("Not intersecting {} {}", scan, scanCost);
iter.remove();
}
}
}
Function<? super IndexScan,Void> hook = intersectionEnumerationHook;
for (Iterator<IndexScan> iterator = enumerator.iterator(); iterator.hasNext(); ) {
IndexScan intersectedIndex = iterator.next();
if (hook != null)
hook.apply(intersectedIndex);
setIntersectionConditions(intersectedIndex);
intersectedIndex.setCovering(determineCovering(intersectedIndex));
intersectedIndex.setCostEstimate(estimateCost(intersectedIndex));
if (previousBest == null) {
logger.debug("Selecting {}", intersectedIndex);
previousBest = intersectedIndex;
}
else if (compare(intersectedIndex, previousBest) > 0) {
logger.debug("Preferring {}", intersectedIndex);
previousBest = intersectedIndex;
}
else {
logger.debug("Rejecting {}", intersectedIndex);
// If the scan costs alone are higher than the previous best cost, there's no way this scan or
// any scan that uses it will be the best. Just remove the whole branch.
if (intersectedIndex.getScanCostEstimate().compareTo(previousBest.getCostEstimate()) > 0)
iterator.remove();
}
}
return previousBest;
}
private void setIntersectionConditions(IndexScan rawScan) {
MultiIndexIntersectScan scan = (MultiIndexIntersectScan) rawScan;
if (isAncestor(scan.getOutputIndexScan().getLeafMostTable(),
scan.getSelectorIndexScan().getLeafMostTable())) {
// More conditions up the same branch are safely implied by the output row.
ConditionsCounter<ConditionExpression> counter = new ConditionsCounter<>(conditions.size());
scan.incrementConditionsCounter(counter);
scan.setConditions(new ArrayList<>(counter.getCountedConditions()));
}
else {
// Otherwise only those for the output row are safe and
// conditions on another branch need to be checked again;
scan.setConditions(scan.getOutputIndexScan().getConditions());
}
}
/** Is the given <code>rootTable</code> an ancestor of <code>leafTable</code>? */
private static boolean isAncestor(TableSource leafTable, TableSource rootTable) {
do {
if (leafTable == rootTable)
return true;
leafTable = leafTable.getParentTable();
} while (leafTable != null);
return false;
}
private class IntersectionEnumerator extends MultiIndexEnumerator<ConditionExpression,IndexScan,SingleIndexScan> {
@Override
protected Collection<ConditionExpression> getLeafConditions(SingleIndexScan scan) {
int skips = scan.getPeggedCount();
List<ConditionExpression> conditions = scan.getConditions();
if (conditions == null)
return null;
int nconds = conditions.size();
return ((skips) > 0 && (skips == nconds)) ? conditions : null;
}
@Override
protected IndexScan intersect(IndexScan first, IndexScan second, int comparisons) {
return new MultiIndexIntersectScan(first, second, comparisons);
}
@Override
protected List<Column> getComparisonColumns(IndexScan first, IndexScan second) {
EquivalenceFinder<ColumnExpression> equivs = getColumnEquivalencies();
List<ExpressionNode> firstOrdering = orderingCols(first);
List<ExpressionNode> secondOrdering = orderingCols(second);
int ncols = Math.min(firstOrdering.size(), secondOrdering.size());
List<Column> result = new ArrayList<>(ncols);
for (int i=0; i < ncols; ++i) {
ExpressionNode firstCol = firstOrdering.get(i);
ExpressionNode secondCol = secondOrdering.get(i);
if (!(firstCol instanceof ColumnExpression) || !(secondCol instanceof ColumnExpression))
break;
if (!equivs.areEquivalent((ColumnExpression) firstCol, (ColumnExpression) secondCol))
break;
result.add(((ColumnExpression)firstCol).getColumn());
}
return result;
}
private List<ExpressionNode> orderingCols(IndexScan index) {
List<ExpressionNode> result = index.getColumns();
return result.subList(index.getPeggedCount(), result.size());
}
}
/** Find the best index on the given table.
* @param required Tables reachable from root via INNER joins and hence not nullable.
*/
public IndexScan pickBestIndex(TableGroupJoinNode node, Set<TableSource> required, IntersectionEnumerator enumerator) {
TableSource table = node.getTable();
IndexScan bestIndex = null;
// Can only consider single table indexes when table is not
// nullable (required). If table is the optional part of a
// LEFT join, can still consider compatible LEFT / RIGHT group
// indexes, below. WHERE conditions are removed before this is
// called, see GroupIndexGoal#setJoinConditions().
if (required.contains(table)) {
for (TableIndex index : table.getTable().getTable().getIndexes()) {
SingleIndexScan candidate = new SingleIndexScan(index, table, queryContext);
bestIndex = betterIndex(bestIndex, candidate, enumerator);
}
}
if ((table.getGroup() != null) && !hasOuterJoinNonGroupConditions(node)) {
for (GroupIndex index : table.getGroup().getGroup().getIndexes()) {
// The leaf must be used or else we'll get duplicates from a
// scan (the indexed columns need not be root to leaf, making
// ancestors discontiguous and duplicates hard to eliminate).
if (index.leafMostTable() != table.getTable().getTable())
continue;
TableSource rootTable = table;
TableSource rootRequired = null, leafRequired = null;
if (index.getJoinType() == JoinType.LEFT) {
while (rootTable != null) {
if (required.contains(rootTable)) {
rootRequired = rootTable;
if (leafRequired == null)
leafRequired = rootTable;
}
else {
if (leafRequired != null) {
// Optional above required, not LEFT join compatible.
leafRequired = null;
break;
}
}
if (index.rootMostTable() == rootTable.getTable().getTable())
break;
rootTable = rootTable.getParentTable();
}
// The root must be present, since a LEFT index
// does not contain orphans.
if ((rootTable == null) ||
(rootRequired != rootTable) ||
(leafRequired == null))
continue;
}
else {
if (!required.contains(table))
continue;
leafRequired = table;
boolean optionalSeen = false;
while (rootTable != null) {
if (required.contains(rootTable)) {
if (optionalSeen) {
// Required above optional, not RIGHT join compatible.
rootRequired = null;
break;
}
rootRequired = rootTable;
}
else {
optionalSeen = true;
}
if (index.rootMostTable() == rootTable.getTable().getTable())
break;
rootTable = rootTable.getParentTable();
}
// TODO: There are no INNER JOIN group indexes,
// but this would support them.
/*
if (optionalSeen && (index.getJoinType() == JoinType.INNER))
continue;
*/
if ((rootTable == null) ||
(rootRequired == null))
continue;
}
SingleIndexScan candidate = new SingleIndexScan(index, rootTable,
rootRequired, leafRequired,
table, queryContext);
bestIndex = betterIndex(bestIndex, candidate, enumerator);
}
}
return bestIndex;
}
// If a LEFT join has more conditions, they won't be included in an index, so
// can't use it.
protected boolean hasOuterJoinNonGroupConditions(TableGroupJoinNode node) {
if (node.getTable().isRequired())
return false;
ConditionList conditions = node.getJoinConditions();
if (conditions != null) {
for (ConditionExpression cond : conditions) {
if (cond.getImplementation() != ConditionExpression.Implementation.GROUP_JOIN) {
return true;
}
}
}
return false;
}
protected IndexScan betterIndex(IndexScan bestIndex, SingleIndexScan candidate, IntersectionEnumerator enumerator) {
if (usable(candidate)) {
enumerator.addLeaf(candidate);
if (bestIndex == null) {
logger.debug("Selecting {}", candidate);
return candidate;
}
else if (compare(candidate, bestIndex) > 0) {
logger.debug("Preferring {}", candidate);
return candidate;
}
else {
logger.debug("Rejecting {}", candidate);
}
}
return bestIndex;
}
private GroupLoopScan pickBestGroupLoop() {
GroupLoopScan bestScan = null;
Set<TableSource> outsideSameGroup = new HashSet<>(tables.getGroup().getTables());
outsideSameGroup.retainAll(boundTables);
for (TableGroupJoin join : tables.getGroup().getRejectedJoins()) {
TableSource parent = join.getParent();
TableSource child = join.getChild();
TableSource inside, outside;
boolean insideIsParent;
if (outsideSameGroup.contains(parent) && tables.containsTable(child)) {
inside = child;
outside = parent;
insideIsParent = false;
}
else if (outsideSameGroup.contains(child) && tables.containsTable(parent)) {
inside = parent;
outside = child;
insideIsParent = true;
}
else {
continue;
}
if (mightFlattenOrSort(outside)) {
continue; // Lookup_Nested won't be allowed.
}
GroupLoopScan forJoin = new GroupLoopScan(inside, outside, insideIsParent,
join.getConditions());
determineRequiredTables(forJoin);
forJoin.setCostEstimate(estimateCost(forJoin));
if (bestScan == null) {
logger.debug("Selecting {}", forJoin);
bestScan = forJoin;
}
else if (compare(forJoin, bestScan) > 0) {
logger.debug("Preferring {}", forJoin);
bestScan = forJoin;
}
else {
logger.debug("Rejecting {}", forJoin);
}
}
return bestScan;
}
private boolean mightFlattenOrSort(TableSource table) {
if (!(table.getOutput() instanceof TableGroupJoinTree))
return true; // Don't know; be conservative.
TableGroupJoinTree tree = (TableGroupJoinTree)table.getOutput();
TableGroupJoinNode root = tree.getRoot();
if (root.getTable() != table)
return true;
if (root.getFirstChild() != null)
return true;
// Only table in this join tree, shouldn't flatten.
PlanNode output = tree;
do {
output = output.getOutput();
if (output instanceof Sort)
return true;
if (output instanceof ResultSet)
break;
} while (output != null);
return false; // No Sort, either.
}
private ExpressionsHKeyScan pickHKeyRow(TableGroupJoinNode node, Set<TableSource> required) {
TableSource table = node.getTable();
if (!required.contains(table)) return null;
ExpressionsHKeyScan scan = new ExpressionsHKeyScan(table);
HKey hKey = scan.getHKey();
int ncols = hKey.nColumns();
List<ExpressionNode> columns = new ArrayList<>(ncols);
for (int i = 0; i < ncols; i++) {
ExpressionNode column = getColumnExpression(table, hKey.column(i));
if (column == null) return null;
columns.add(column);
}
scan.setColumns(columns);
int nequals = insertLeadingEqualities(scan, conditions);
if (nequals != ncols) return null;
required = new HashSet<>(required);
// We do not handle any actual data columns.
required.addAll(requiredColumns.getTables());
scan.setRequiredTables(required);
scan.setCostEstimate(estimateCost(scan));
logger.debug("Selecting {}", scan);
return scan;
}
public int compare(BaseScan i1, BaseScan i2) {
return i2.getCostEstimate().compareTo(i1.getCostEstimate());
}
protected boolean determineCovering(IndexScan index) {
// Include the non-condition requirements.
RequiredColumns requiredAfter = new RequiredColumns(requiredColumns);
RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter);
// Add in any conditions not handled by the index.
for (ConditionExpression condition : conditions) {
boolean found = false;
if (index.getConditions() != null) {
for (ConditionExpression indexCondition : index.getConditions()) {
if (indexCondition == condition) {
found = true;
break;
}
}
}
if (!found)
condition.accept(filler);
}
// Add sort if not handled by the index.
if ((queryGoal.getOrdering() != null) &&
(index.getOrderEffectiveness() != IndexScan.OrderEffectiveness.SORTED)) {
// Only this node, not its inputs.
filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering()));
queryGoal.getOrdering().accept(filler);
}
// Record what tables are required: within the index if any
// columns still needed, others if joined at all. Do this
// before taking account of columns from a covering index,
// since may not use it that way.
{
Collection<TableSource> joined = index.getTables();
Set<TableSource> required = new HashSet<>();
boolean moreTables = false;
for (TableSource table : requiredAfter.getTables()) {
if (!joined.contains(table)) {
moreTables = true;
required.add(table);
}
else if (requiredAfter.hasColumns(table) ||
(table == queryGoal.getUpdateTarget())) {
required.add(table);
}
}
index.setRequiredTables(required);
if (moreTables)
// Need to join up last the index; index might point
// to an orphan.
return false;
}
if (queryGoal.getUpdateTarget() != null) {
// UPDATE statements need the whole target row and are thus never
// covering for their group.
for (TableGroupJoinNode table : tables) {
if (table.getTable() == queryGoal.getUpdateTarget())
return false;
}
}
// Remove the columns we do have from the index.
int ncols = index.getColumns().size();
for (int i = 0; i < ncols; i++) {
ExpressionNode column = index.getColumns().get(i);
if ((column instanceof ColumnExpression) && index.isRecoverableAt(i)) {
if (requiredAfter.have((ColumnExpression)column) &&
(i >= index.getNKeyColumns())) {
index.setUsesAllColumns(true);
}
}
}
return requiredAfter.isEmpty();
}
protected void determineRequiredTables(GroupLoopScan scan) {
// Include the non-condition requirements.
RequiredColumns requiredAfter = new RequiredColumns(requiredColumns);
RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter);
// Add in any non-join conditions.
for (ConditionExpression condition : conditions) {
boolean found = false;
for (ConditionExpression joinCondition : scan.getJoinConditions()) {
if (joinCondition == condition) {
found = true;
break;
}
}
if (!found)
condition.accept(filler);
}
// Does not sort.
if (queryGoal.getOrdering() != null) {
// Only this node, not its inputs.
filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering()));
queryGoal.getOrdering().accept(filler);
}
// The only table we can exclude is the one initially joined to, in the case
// where all the data comes from elsewhere on that branch.
Set<TableSource> required = new HashSet<>(requiredAfter.getTables());
if ((required.size() > 1) &&
!requiredAfter.hasColumns(scan.getInsideTable()))
required.remove(scan.getInsideTable());
scan.setRequiredTables(required);
}
public CostEstimate estimateCost(IndexScan index) {
return estimateCost(index, queryGoal.getLimit());
}
public CostEstimate estimateCost(IndexScan index, long limit) {
PlanCostEstimator estimator = newEstimator();
Set<TableSource> requiredTables = index.getRequiredTables();
estimator.indexScan(index);
if (!index.isCovering()) {
estimator.flatten(tables,
index.getLeafMostTable(), requiredTables);
}
Collection<ConditionExpression> unhandledConditions =
new HashSet<>(requiredConditions);
if (index.getConditions() != null)
unhandledConditions.removeAll(index.getConditions());
if (!unhandledConditions.isEmpty()) {
estimator.select(unhandledConditions,
selectivityConditions(unhandledConditions, requiredTables));
}
if (queryGoal.needSort(index.getOrderEffectiveness())) {
estimator.sort(queryGoal.sortFields());
}
estimator.setLimit(limit);
return estimator.getCostEstimate();
}
public CostEstimate estimateIntersectionCost(IndexScan index) {
if (index.getOrderEffectiveness() == IndexScan.OrderEffectiveness.NONE)
return index.getScanCostEstimate();
long limit = queryGoal.getLimit();
if (limit < 0)
return index.getScanCostEstimate();
// There is a limit and this index looks to be sorted, so adjust for that
// limit. Otherwise, the scan only cost, which includes all rows, will appear
// too large compared to a limit-aware best plan.
PlanCostEstimator estimator = newEstimator();
estimator.indexScan(index);
estimator.setLimit(limit);
return estimator.getCostEstimate();
}
public CostEstimate estimateCost(GroupScan scan) {
PlanCostEstimator estimator = newEstimator();
Set<TableSource> requiredTables = requiredColumns.getTables();
estimator.groupScan(scan, tables, requiredTables);
if (!requiredConditions.isEmpty()) {
estimator.select(requiredConditions,
selectivityConditions(requiredConditions, requiredTables));
}
estimator.setLimit(queryGoal.getLimit());
return estimator.getCostEstimate();
}
public CostEstimate estimateCost(GroupLoopScan scan) {
PlanCostEstimator estimator = newEstimator();
Set<TableSource> requiredTables = scan.getRequiredTables();
estimator.groupLoop(scan, tables, requiredTables);
Collection<ConditionExpression> unhandledConditions =
new HashSet<>(requiredConditions);
addInnerJoinConditions(unhandledConditions, scan.getInsideTable());
unhandledConditions.removeAll(scan.getJoinConditions());
if (!unhandledConditions.isEmpty()) {
estimator.select(unhandledConditions,
selectivityConditions(unhandledConditions, requiredTables));
}
if (queryGoal.needSort(IndexScan.OrderEffectiveness.NONE)) {
estimator.sort(queryGoal.sortFields());
}
estimator.setLimit(queryGoal.getLimit());
return estimator.getCostEstimate();
}
private void addInnerJoinConditions(Collection<ConditionExpression> unhandledConditions, TableSource insideTable) {
PlanWithInput output = insideTable.getOutput();
while (output != null && !(output instanceof BaseQuery))
{
if (output instanceof JoinNode) {
JoinNode joinNode = (JoinNode) output;
if (joinNode.isInnerJoin()) {
unhandledConditions.addAll(joinNode.getJoinConditions());
this.conditionSources.add(joinNode.getJoinConditions());
}
// the join conditions are supposed to be pushed down as far as they can be at this point, so once we
// hit a join node, we're done
break;
}
output = output.getOutput();
}
}
public CostEstimate estimateCost(ExpressionsHKeyScan scan) {
PlanCostEstimator estimator = newEstimator();
Set<TableSource> requiredTables = scan.getRequiredTables();
estimator.hKeyRow(scan);
estimator.flatten(tables, scan.getTable(), requiredTables);
Collection<ConditionExpression> unhandledConditions =
new HashSet<>(requiredConditions);
unhandledConditions.removeAll(scan.getConditions());
if (!unhandledConditions.isEmpty()) {
estimator.select(unhandledConditions,
selectivityConditions(unhandledConditions, requiredTables));
}
if (queryGoal.needSort(IndexScan.OrderEffectiveness.NONE)) {
estimator.sort(queryGoal.sortFields());
}
estimator.setLimit(queryGoal.getLimit());
return estimator.getCostEstimate();
}
public double estimateSelectivity(IndexScan index) {
return queryGoal.getCostEstimator().conditionsSelectivity(selectivityConditions(index.getConditions(), index.getTables()));
}
// Conditions that might have a recognizable selectivity.
protected SelectivityConditions selectivityConditions(Collection<ConditionExpression> conditions, Collection<TableSource> requiredTables) {
SelectivityConditions result = new SelectivityConditions();
if (conditions == null) {
return result;
}
for (ConditionExpression condition : conditions) {
if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
if (ccond.getLeft() instanceof ColumnExpression) {
ColumnExpression column = (ColumnExpression)ccond.getLeft();
if ((column.getColumn() != null) &&
requiredTables.contains(column.getTable()) &&
constantOrBound(ccond.getRight())) {
result.addCondition(column, condition);
}
}
}
else if (condition instanceof InListCondition) {
InListCondition incond = (InListCondition)condition;
if (incond.getOperand() instanceof ColumnExpression) {
ColumnExpression column = (ColumnExpression)incond.getOperand();
if ((column.getColumn() != null) &&
requiredTables.contains(column.getTable())) {
boolean allConstant = true;
for (ExpressionNode expr : incond.getExpressions()) {
if (!constantOrBound(expr)) {
allConstant = false;
break;
}
}
if (allConstant) {
result.addCondition(column, condition);
}
}
}
}
}
return result;
}
// Recognize the case of a join that is only used for predication.
// TODO: This is only covers the simplest case, namely an index that is unique
// none of whose columns are actually used.
public boolean semiJoinEquivalent(BaseScan scan) {
if (scan instanceof SingleIndexScan) {
SingleIndexScan indexScan = (SingleIndexScan)scan;
if (indexScan.isCovering() && isUnique(indexScan) &&
requiredColumns.isEmpty()) {
return true;
}
}
return false;
}
// Does this scan return at most one row?
protected boolean isUnique(SingleIndexScan indexScan) {
List<ExpressionNode> equalityComparands = indexScan.getEqualityComparands();
if (equalityComparands == null)
return false;
int nequals = equalityComparands.size();
Index index = indexScan.getIndex();
if (index.isUnique() && (nequals >= index.getKeyColumns().size()))
return true;
if (index.isGroupIndex())
return false;
Set<Column> equalityColumns = new HashSet<>(nequals);
for (int i = 0; i < nequals; i++) {
ExpressionNode equalityExpr = indexScan.getColumns().get(i);
if (equalityExpr instanceof ColumnExpression) {
equalityColumns.add(((ColumnExpression)equalityExpr).getColumn());
}
}
TableIndex tableIndex = (TableIndex)index;
find_index: // Find a unique index all of whose columns are equaled.
for (TableIndex otherIndex : tableIndex.getTable().getIndexes()) {
if (!otherIndex.isUnique()) continue;
for (IndexColumn otherColumn : otherIndex.getKeyColumns()) {
if (!equalityColumns.contains(otherColumn.getColumn()))
continue find_index;
}
return true;
}
return false;
}
public JoinAndIndexPicker.Plan.JoinableWithConditionsToRemove install(BaseScan scan,
List<ConditionList> conditionSources,
boolean sortAllowed, boolean copy) {
TableGroupJoinTree result = tables;
List<? extends ConditionExpression> conditionsToRemove;
// Need to have more than one copy of this tree in the final result.
if (copy) result = new TableGroupJoinTree(result.getRoot());
result.setScan(scan);
this.sortAllowed = sortAllowed;
if (scan instanceof IndexScan) {
IndexScan indexScan = (IndexScan)scan;
if (indexScan instanceof MultiIndexIntersectScan) {
MultiIndexIntersectScan multiScan = (MultiIndexIntersectScan)indexScan;
installOrdering(indexScan, multiScan.getOrdering(), multiScan.getPeggedCount(), multiScan.getComparisonFields());
}
installConditions(indexScan.getConditions(), conditionSources);
conditionsToRemove = indexScan.getConditions();
if (sortAllowed)
queryGoal.installOrderEffectiveness(indexScan.getOrderEffectiveness());
}
else {
if (scan instanceof GroupLoopScan) {
GroupLoopScan groupScan = (GroupLoopScan) scan;
installConditions(groupScan.getJoinConditions(),
conditionSources);
conditionsToRemove = groupScan.getJoinConditions();
}
else if (scan instanceof FullTextScan) {
FullTextScan textScan = (FullTextScan)scan;
installConditions(textScan.getConditions(), conditionSources);
conditionsToRemove = textScan.getConditions();
if (conditions.isEmpty()) {
textScan.setLimit((int)queryGoal.getLimit());
}
}
else if (scan instanceof ExpressionsHKeyScan) {
ExpressionsHKeyScan hKeyScan = (ExpressionsHKeyScan) scan;
installConditions(hKeyScan.getConditions(),
conditionSources);
conditionsToRemove = hKeyScan.getConditions();
}
else {
conditionsToRemove = new ConditionList();
}
if (sortAllowed)
queryGoal.installOrderEffectiveness(IndexScan.OrderEffectiveness.NONE);
}
return new JoinAndIndexPicker.Plan.JoinableWithConditionsToRemove(result, conditionsToRemove);
}
/** Change WHERE as a consequence of <code>index</code> being
* used, using either the sources returned by {@link #updateContext} or the
* current ones if nothing has been changed.
*/
public void installConditions(Collection<? extends ConditionExpression> conditions,
List<ConditionList> conditionSources) {
if (conditions != null) {
if (conditionSources == null)
conditionSources = this.conditionSources;
for (ConditionExpression condition : conditions) {
for (ConditionList conditionSource : conditionSources) {
if (conditionSource.remove(condition))
break;
}
}
}
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append(tables.summaryString(PlanNode.SummaryConfiguration.DEFAULT));
str.append("\n");
str.append(conditions);
str.append("\n[");
boolean first = true;
for (ColumnSource bound : boundTables) {
if (first)
first = false;
else
str.append(", ");
str.append(bound.getName());
}
str.append("]");
return str.toString();
}
// Too-many-way UNION can consume too many resources (and overflow
// the stack explaining).
protected static int COLUMN_RANGE_MAX_SEGMENTS_DEFAULT = 16;
// Get Range-expressible conditions for given column.
protected ColumnRanges rangeForIndex(ExpressionNode expressionNode) {
if (expressionNode instanceof ColumnExpression) {
if (columnsToRanges == null) {
columnsToRanges = new HashMap<>();
for (ConditionExpression condition : conditions) {
ColumnRanges range = ColumnRanges.rangeAtNode(condition);
if (range != null) {
ColumnExpression rangeColumn = range.getColumnExpression();
ColumnRanges oldRange = columnsToRanges.get(rangeColumn);
if (oldRange != null)
range = ColumnRanges.andRanges(range, oldRange);
columnsToRanges.put(rangeColumn, range);
}
}
if (!columnsToRanges.isEmpty()) {
int maxSegments;
String prop = queryGoal.getRulesContext().getProperty("columnRangeMaxSegments");
if (prop != null)
maxSegments = Integer.parseInt(prop);
else
maxSegments = COLUMN_RANGE_MAX_SEGMENTS_DEFAULT;
Iterator<ColumnRanges> iter = columnsToRanges.values().iterator();
while (iter.hasNext()) {
if (iter.next().getSegments().size() > maxSegments) {
iter.remove();
}
}
}
}
ColumnExpression columnExpression = (ColumnExpression)expressionNode;
return columnsToRanges.get(columnExpression);
}
return null;
}
static class RequiredColumns {
private Map<TableSource,Set<ColumnExpression>> map;
public RequiredColumns(TableGroupJoinTree tables) {
map = new HashMap<>();
for (TableGroupJoinNode table : tables) {
map.put(table.getTable(), new HashSet<ColumnExpression>());
}
}
public RequiredColumns(RequiredColumns other) {
map = new HashMap<>(other.map.size());
for (Map.Entry<TableSource,Set<ColumnExpression>> entry : other.map.entrySet()) {
map.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
}
public Set<TableSource> getTables() {
return map.keySet();
}
public boolean hasColumns(TableSource table) {
Set<ColumnExpression> entry = map.get(table);
if (entry == null) return false;
return !entry.isEmpty();
}
public boolean isEmpty() {
boolean empty = true;
for (Set<ColumnExpression> entry : map.values())
if (!entry.isEmpty())
return false;
return empty;
}
public void require(ColumnExpression expr) {
Set<ColumnExpression> entry = map.get(expr.getTable());
if (entry != null)
entry.add(expr);
}
/** Opposite of {@link #require}: note that we have a source for this column. */
public boolean have(ColumnExpression expr) {
Set<ColumnExpression> entry = map.get(expr.getTable());
if (entry != null)
return entry.remove(expr);
else
return false;
}
public void clear() {
for (Set<ColumnExpression> entry : map.values())
entry.clear();
}
}
static class RequiredColumnsFiller implements PlanVisitor, ExpressionVisitor {
private RequiredColumns requiredColumns;
private Map<PlanNode,Void> excludedPlanNodes, includedPlanNodes;
private Map<ExpressionNode,Void> excludedExpressions;
private Deque<Boolean> excludeNodeStack = new ArrayDeque<>();
private boolean excludeNode = false;
private int excludeDepth = 0;
private int subqueryDepth = 0;
public RequiredColumnsFiller(RequiredColumns requiredColumns) {
this.requiredColumns = requiredColumns;
}
public RequiredColumnsFiller(RequiredColumns requiredColumns,
Collection<PlanNode> excludedPlanNodes,
Collection<ConditionExpression> excludedExpressions) {
this.requiredColumns = requiredColumns;
this.excludedPlanNodes = new IdentityHashMap<>();
for (PlanNode planNode : excludedPlanNodes)
this.excludedPlanNodes.put(planNode, null);
this.excludedExpressions = new IdentityHashMap<>();
for (ConditionExpression condition : excludedExpressions)
this.excludedExpressions.put(condition, null);
}
public void setIncludedPlanNodes(Collection<PlanNode> includedPlanNodes) {
this.includedPlanNodes = new IdentityHashMap<>();
for (PlanNode planNode : includedPlanNodes)
this.includedPlanNodes.put(planNode, null);
}
@Override
public boolean visitEnter(PlanNode n) {
// Input nodes are called within the context of their output.
// We want to know whether just this node is excluded, not
// it and all its inputs.
excludeNodeStack.push(excludeNode);
excludeNode = exclude(n);
if ((n instanceof Subquery) &&
!((Subquery)n).getOuterTables().isEmpty())
// TODO: Might be accessing tables from outer query as
// group joins, which we don't support currently. Make
// sure those aren't excluded.
subqueryDepth++;
return visit(n);
}
@Override
public boolean visitLeave(PlanNode n) {
excludeNode = excludeNodeStack.pop();
if ((n instanceof Subquery) &&
!((Subquery)n).getOuterTables().isEmpty())
subqueryDepth--;
return true;
}
@Override
public boolean visit(PlanNode n) {
return true;
}
@Override
public boolean visitEnter(ExpressionNode n) {
if (!excludeNode && exclude(n))
excludeDepth++;
return visit(n);
}
@Override
public boolean visitLeave(ExpressionNode n) {
if (!excludeNode && exclude(n))
excludeDepth--;
return true;
}
@Override
public boolean visit(ExpressionNode n) {
if (!excludeNode && (excludeDepth == 0)) {
if (n instanceof ColumnExpression)
requiredColumns.require((ColumnExpression)n);
}
return true;
}
// Should this plan node be excluded from the requirement?
protected boolean exclude(PlanNode node) {
if (includedPlanNodes != null)
return !includedPlanNodes.containsKey(node);
else if (excludedPlanNodes != null)
return excludedPlanNodes.containsKey(node);
else
return false;
}
// Should this expression be excluded from requirement?
protected boolean exclude(ExpressionNode expr) {
return (((excludedExpressions != null) &&
excludedExpressions.containsKey(expr)) ||
// Group join conditions are handled specially.
((expr instanceof ConditionExpression) &&
(((ConditionExpression)expr).getImplementation() ==
ConditionExpression.Implementation.GROUP_JOIN) &&
// Include expressions in subqueries until do joins across them.
(subqueryDepth == 0)));
}
}
/* Spatial indexes */
/** For now, a spatial index is a special kind of table index on
* Z-order of two coordinates.
*/
public boolean spatialUsable(SingleIndexScan index, int nequals) {
// Look for a spatial-join compatible predicate one of whose operands
// matches the spatial index definition.
// There are two additional legacy cases to recognize (for the time being):
// WHERE distance_lat_lon(column_lat, column_lon, start_lat, start_lon) <= radius
// ORDER BY znear(column_lat, column_lon, start_lat, start_lon), which
// means fan out from that center in Z-order.
ExpressionNode nextColumn = index.getColumns().get(nequals);
if (!(nextColumn instanceof SpecialIndexExpression))
return false; // Did not have enough equalities to get to spatial part.
SpecialIndexExpression indexExpression = (SpecialIndexExpression)nextColumn;
SpecialIndexExpression.Function spatialFunction = indexExpression.getFunction();
List<ExpressionNode> operands = indexExpression.getOperands();
boolean matched = false;
for (ConditionExpression condition : conditions) {
ExpressionNode spatialJoinTo = null;
if (condition instanceof FunctionCondition) {
FunctionCondition fcond = (FunctionCondition)condition;
spatialJoinTo = matchSpatialPredicate(spatialFunction, operands, fcond);
}
else if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
switch (ccond.getOperation()) {
case LE:
case LT:
spatialJoinTo = matchDistanceLatLon(spatialFunction, operands,
ccond.getLeft(),
ccond.getRight());
break;
case GE:
case GT:
spatialJoinTo = matchDistanceLatLon(spatialFunction, operands,
ccond.getRight(),
ccond.getLeft());
break;
}
}
if (spatialJoinTo != null) {
index.setLowComparand(spatialJoinTo, true);
index.setHighComparand(spatialJoinTo, true);
index.setOrderEffectiveness(IndexScan.OrderEffectiveness.NONE);
matched = true;
break;
}
}
if (!matched) {
if (sortAllowed && (queryGoal.getOrdering() != null)) {
List<OrderByExpression> orderBy = queryGoal.getOrdering().getOrderBy();
if (orderBy.size() == 1) {
ExpressionNode spatialJoinTo = matchZnear(spatialFunction, operands,
orderBy.get(0));
if (spatialJoinTo != null) {
index.setLowComparand(spatialJoinTo, true);
index.setOrderEffectiveness(IndexScan.OrderEffectiveness.SORTED);
matched = true;
}
}
}
if (!matched)
return false;
}
index.setCovering(determineCovering(index));
index.setCostEstimate(estimateCostSpatial(index));
return true;
}
private ExpressionNode matchSpatialPredicate(SpecialIndexExpression.Function indexFunction,
List<ExpressionNode> indexExpressions,
FunctionCondition cond) {
String function = cond.getFunction();
List<ExpressionNode> operands = cond.getOperands();
for (GeoOverlaps.OverlapType overlap : GeoOverlaps.OverlapType.values()) {
if (function.equalsIgnoreCase(overlap.functionName())) {
ExpressionNode op1 = operands.get(0);
ExpressionNode op2 = operands.get(1);
if (matchSpatialIndex(indexFunction, indexExpressions, op1) &&
constantOrBound(op2)) {
return op2;
}
else if (matchSpatialIndex(indexFunction, indexExpressions, op2) &&
constantOrBound(op1)) {
return op1;
}
}
}
if (function.equalsIgnoreCase("geo_within_distance")) {
ExpressionNode op1 = operands.get(0);
ExpressionNode op2 = operands.get(1);
ExpressionNode radius = operands.get(2);
if (constantOrBound(radius)) {
if (matchSpatialIndex(indexFunction, indexExpressions, op1) &&
constantOrBound(op2)) {
return spatialWithinDistance(op2, radius);
}
if (matchSpatialIndex(indexFunction, indexExpressions, op2) &&
constantOrBound(op1)) {
return spatialWithinDistance(op1, radius);
}
}
}
return null;
}
private boolean matchSpatialIndex(SpecialIndexExpression.Function indexFunction,
List<ExpressionNode> indexExpressions,
ExpressionNode expr) {
if (!(expr instanceof FunctionExpression))
return false;
FunctionExpression func = (FunctionExpression)expr;
return (func.getFunction().equalsIgnoreCase(indexFunction.functionName()) &&
func.getOperands().equals(indexExpressions));
}
private ExpressionNode spatialWithinDistance(ExpressionNode op, ExpressionNode distance) {
return new FunctionExpression("geo_expanded_envelope",
Arrays.asList(op, distance),
null, null, null);
}
private ExpressionNode spatialPoint(ExpressionNode lat, ExpressionNode lon) {
return new FunctionExpression("geo_lat_lon",
Arrays.asList(lat, lon),
null, null, null);
}
private ExpressionNode matchDistanceLatLon(SpecialIndexExpression.Function function,
List<ExpressionNode> indexExpressions,
ExpressionNode left, ExpressionNode right) {
if (!((function == SpecialIndexExpression.Function.GEO_LAT_LON) &&
(left instanceof FunctionExpression) &&
((FunctionExpression)left).getFunction().equalsIgnoreCase("distance_lat_lon") &&
constantOrBound(right)))
return null;
ExpressionNode col1 = indexExpressions.get(0);
ExpressionNode col2 = indexExpressions.get(1);
List<ExpressionNode> operands = ((FunctionExpression)left).getOperands();
if (operands.size() != 4) return null; // TODO: Would error here be better?
ExpressionNode op1 = operands.get(0);
ExpressionNode op2 = operands.get(1);
ExpressionNode op3 = operands.get(2);
ExpressionNode op4 = operands.get(3);
/* TODO: Should not be needed.
if ((right.getType() != null) &&
(right.getType().typeClass().jdbcType() != Types.DECIMAL)) {
DataTypeDescriptor sqlType =
new DataTypeDescriptor(TypeId.DECIMAL_ID, 10, 6, true, 12);
TInstance type = queryGoal.getRulesContext()
.getTypesTranslator().typeForSQLType(sqlType);
right = new CastExpression(right, sqlType, right.getSQLsource(), type);
}
*/
if (columnMatches(col1, op1) && columnMatches(col2, op2) &&
constantOrBound(op3) && constantOrBound(op4)) {
return spatialWithinDistance(spatialPoint(op3, op4), right);
}
if (columnMatches(col1, op3) && columnMatches(col2, op4) &&
constantOrBound(op1) && constantOrBound(op2)) {
return spatialWithinDistance(spatialPoint(op1, op2), right);
}
return null;
}
private ExpressionNode matchZnear(SpecialIndexExpression.Function function,
List<ExpressionNode> indexExpressions,
OrderByExpression orderBy) {
if (!((function == SpecialIndexExpression.Function.GEO_LAT_LON) &&
orderBy.isAscending()))
return null;
ExpressionNode orderExpr = orderBy.getExpression();
if (!((orderExpr instanceof FunctionExpression) &&
((FunctionExpression)orderExpr).getFunction().equalsIgnoreCase("znear")))
return null;
ExpressionNode col1 = indexExpressions.get(0);
ExpressionNode col2 = indexExpressions.get(1);
List<ExpressionNode> operands = ((FunctionExpression)orderExpr).getOperands();
if (operands.size() != 4) return null; // TODO: Would error here be better?
ExpressionNode op1 = operands.get(0);
ExpressionNode op2 = operands.get(1);
ExpressionNode op3 = operands.get(2);
ExpressionNode op4 = operands.get(3);
if (columnMatches(col1, op1) && columnMatches(col2, op2) &&
constantOrBound(op3) && constantOrBound(op4))
return new FunctionExpression("geo_lat_lon",
Arrays.asList(op3, op4),
null, null, null);
if (columnMatches(col1, op3) && columnMatches(col2, op4) &&
constantOrBound(op1) && constantOrBound(op2))
return new FunctionExpression("geo_lat_lon",
Arrays.asList(op1, op2),
null, null, null);
return null;
}
private static boolean columnMatches(ExpressionNode col, ExpressionNode op) {
if (op instanceof CastExpression)
op = ((CastExpression)op).getOperand();
return col.equals(op);
}
public CostEstimate estimateCostSpatial(SingleIndexScan index) {
PlanCostEstimator estimator = newEstimator();
Set<TableSource> requiredTables = requiredColumns.getTables();
estimator.spatialIndex(index);
if (!index.isCovering()) {
estimator.flatten(tables, index.getLeafMostTable(), requiredTables);
}
Collection<ConditionExpression> unhandledConditions = new HashSet<>(requiredConditions);
if (index.getConditions() != null)
unhandledConditions.removeAll(index.getConditions());
if (!unhandledConditions.isEmpty()) {
estimator.select(unhandledConditions,
selectivityConditions(unhandledConditions, requiredTables));
}
if (queryGoal.needSort(index.getOrderEffectiveness())) {
estimator.sort(queryGoal.sortFields());
}
estimator.setLimit(queryGoal.getLimit());
return estimator.getCostEstimate();
}
protected FullTextScan pickFullText() {
List<ConditionExpression> textConditions = new ArrayList<>(0);
for (ConditionExpression condition : conditions) {
if ((condition instanceof FunctionExpression) &&
((FunctionExpression)condition).getFunction().equalsIgnoreCase("full_text_search")) {
textConditions.add(condition);
}
}
if (textConditions.isEmpty())
return null;
List<FullTextField> textFields = new ArrayList<>(0);
FullTextQuery query = null;
for (ConditionExpression condition : textConditions) {
List<ExpressionNode> operands = ((FunctionExpression)condition).getOperands();
FullTextQuery clause = null;
switch (operands.size()) {
case 1:
clause = fullTextBoolean(operands.get(0), textFields);
if (clause == null) continue;
break;
case 2:
if ((operands.get(0) instanceof ColumnExpression) &&
constantOrBound(operands.get(1))) {
ColumnExpression column = (ColumnExpression)operands.get(0);
if (column.getTable() instanceof TableSource) {
if (!tables.containsTable((TableSource)column.getTable()))
continue;
FullTextField field = new FullTextField(column,
FullTextField.Type.PARSE,
operands.get(1));
textFields.add(field);
clause = field;
}
}
break;
}
if (clause == null)
throw new UnsupportedSQLException("Unrecognized FULL_TEXT_SEARCH call",
condition.getSQLsource());
if (query == null) {
query = clause;
}
else {
query = fullTextBoolean(Arrays.asList(query, clause),
Arrays.asList(FullTextQueryBuilder.BooleanType.MUST,
FullTextQueryBuilder.BooleanType.MUST));
}
}
if (query == null)
return null;
FullTextIndex foundIndex = null;
TableSource foundTable = null;
find_index:
for (FullTextIndex index : textFields.get(0).getColumn().getColumn().getTable().getFullTextIndexes()) {
TableSource indexTable = null;
for (FullTextField textField : textFields) {
Column column = textField.getColumn().getColumn();
boolean found = false;
for (IndexColumn indexColumn : index.getKeyColumns()) {
if (indexColumn.getColumn() == column) {
if (foundIndex == null) {
textField.setIndexColumn(indexColumn);
}
found = true;
if ((indexTable == null) &&
(indexColumn.getColumn().getTable() == index.getIndexedTable())) {
indexTable = (TableSource)textField.getColumn().getTable();
}
break;
}
}
if (!found) {
continue find_index;
}
}
if (foundIndex == null) {
foundIndex = index;
foundTable = indexTable;
}
else {
throw new UnsupportedSQLException("Ambiguous full text index: " +
foundIndex + " and " + index);
}
}
if (foundIndex == null) {
StringBuilder str = new StringBuilder("No full text index for: ");
boolean first = true;
for (FullTextField textField : textFields) {
if (first)
first = false;
else
str.append(", ");
str.append(textField.getColumn());
}
throw new UnsupportedSQLException(str.toString());
}
if (foundTable == null) {
for (TableGroupJoinNode node : tables) {
if (node.getTable().getTable().getTable() == foundIndex.getIndexedTable()) {
foundTable = node.getTable();
break;
}
}
}
query = normalizeFullTextQuery(query);
FullTextScan scan = new FullTextScan(foundIndex, query,
foundTable, textConditions);
determineRequiredTables(scan);
scan.setCostEstimate(estimateCostFullText(scan));
return scan;
}
protected FullTextQuery fullTextBoolean(ExpressionNode condition,
List<FullTextField> textFields) {
if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
if ((ccond.getLeft() instanceof ColumnExpression) &&
constantOrBound(ccond.getRight())) {
ColumnExpression column = (ColumnExpression)ccond.getLeft();
if (column.getTable() instanceof TableSource) {
if (!tables.containsTable((TableSource)column.getTable()))
return null;
FullTextField field = new FullTextField(column,
FullTextField.Type.MATCH,
ccond.getRight());
textFields.add(field);
switch (ccond.getOperation()) {
case EQ:
return field;
case NE:
return fullTextBoolean(Arrays.<FullTextQuery>asList(field),
Arrays.asList(FullTextQueryBuilder.BooleanType.NOT));
}
}
}
}
else if (condition instanceof LogicalFunctionCondition) {
LogicalFunctionCondition lcond = (LogicalFunctionCondition)condition;
String op = lcond.getFunction();
if ("and".equals(op)) {
FullTextQuery left = fullTextBoolean(lcond.getLeft(), textFields);
FullTextQuery right = fullTextBoolean(lcond.getRight(), textFields);
if ((left == null) && (right == null)) return null;
if ((left != null) && (right != null))
return fullTextBoolean(Arrays.asList(left, right),
Arrays.asList(FullTextQueryBuilder.BooleanType.MUST,
FullTextQueryBuilder.BooleanType.MUST));
}
else if ("or".equals(op)) {
FullTextQuery left = fullTextBoolean(lcond.getLeft(), textFields);
FullTextQuery right = fullTextBoolean(lcond.getRight(), textFields);
if ((left == null) && (right == null)) return null;
if ((left != null) && (right != null))
return fullTextBoolean(Arrays.asList(left, right),
Arrays.asList(FullTextQueryBuilder.BooleanType.SHOULD,
FullTextQueryBuilder.BooleanType.SHOULD));
}
else if ("not".equals(op)) {
FullTextQuery inner = fullTextBoolean(lcond.getOperand(), textFields);
if (inner == null)
return null;
else
return fullTextBoolean(Arrays.asList(inner),
Arrays.asList(FullTextQueryBuilder.BooleanType.NOT));
}
}
// TODO: LIKE
throw new UnsupportedSQLException("Cannot convert to full text query" +
condition);
}
protected FullTextQuery fullTextBoolean(List<FullTextQuery> operands,
List<FullTextQueryBuilder.BooleanType> types) {
// Make modifiable copies for normalize.
return new FullTextBoolean(new ArrayList<>(operands),
new ArrayList<>(types));
}
protected FullTextQuery normalizeFullTextQuery(FullTextQuery query) {
if (query instanceof FullTextBoolean) {
FullTextBoolean bquery = (FullTextBoolean)query;
List<FullTextQuery> operands = bquery.getOperands();
List<FullTextQueryBuilder.BooleanType> types = bquery.getTypes();
int i = 0;
while (i < operands.size()) {
FullTextQuery opQuery = operands.get(i);
opQuery = normalizeFullTextQuery(opQuery);
if (opQuery instanceof FullTextBoolean) {
FullTextBoolean opbquery = (FullTextBoolean)opQuery;
List<FullTextQuery> opOperands = opbquery.getOperands();
List<FullTextQueryBuilder.BooleanType> opTypes = opbquery.getTypes();
// Fold in the simplest cases:
// [MUST(x), [MUST(y), MUST(z)]] -> [MUST(x), MUST(y), MUST(z)]
// [MUST(x), [NOT(y)]] -> [MUST(x), NOT(y)]
// [SHOULD(x), [SHOULD(y), SHOULD(z)]] -> [SHOULD(x), SHOULD(y), SHOULD(z)]
boolean fold = true;
switch (types.get(i)) {
case MUST:
check_must:
for (FullTextQueryBuilder.BooleanType opType : opTypes) {
switch (opType) {
case MUST:
case NOT:
break;
default:
fold = false;
break check_must;
}
}
break;
case SHOULD:
check_should:
for (FullTextQueryBuilder.BooleanType opType : opTypes) {
switch (opType) {
case SHOULD:
break;
default:
fold = false;
break check_should;
}
}
break;
default:
fold = false;
break;
}
if (fold) {
for (int j = 0; j < opOperands.size(); j++) {
FullTextQuery opOperand = opOperands.get(j);
FullTextQueryBuilder.BooleanType opType = opTypes.get(j);
if (j == 0) {
operands.set(i, opOperand);
types.set(i, opType);
}
else {
operands.add(i, opOperand);
types.add(i, opType);
}
i++;
}
continue;
}
}
operands.set(i, opQuery);
i++;
}
}
return query;
}
protected void determineRequiredTables(FullTextScan scan) {
// Include the non-condition requirements.
RequiredColumns requiredAfter = new RequiredColumns(requiredColumns);
RequiredColumnsFiller filler = new RequiredColumnsFiller(requiredAfter);
// Add in any non-full-text conditions.
for (ConditionExpression condition : conditions) {
boolean found = false;
for (ConditionExpression scanCondition : scan.getConditions()) {
if (scanCondition == condition) {
found = true;
break;
}
}
if (!found)
condition.accept(filler);
}
// Does not sort.
if (queryGoal.getOrdering() != null) {
// Only this node, not its inputs.
filler.setIncludedPlanNodes(Collections.<PlanNode>singletonList(queryGoal.getOrdering()));
queryGoal.getOrdering().accept(filler);
}
Set<TableSource> required = new HashSet<>(requiredAfter.getTables());
scan.setRequiredTables(required);
}
public CostEstimate estimateCostFullText(FullTextScan scan) {
PlanCostEstimator estimator = newEstimator();
estimator.fullTextScan(scan);
return estimator.getCostEstimate();
}
protected PlanCostEstimator newEstimator() {
return new PlanCostEstimator(queryGoal.getCostEstimator());
}
}