/**
* 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.server.error.UnsupportedSQLException;
import com.foundationdb.sql.optimizer.plan.*;
import com.foundationdb.sql.optimizer.plan.JoinNode.JoinType;
import com.foundationdb.sql.optimizer.plan.TableGroupJoinTree.TableGroupJoinNode;
import com.foundationdb.server.types.texpressions.Comparison;
import com.foundationdb.ais.model.Column;
import com.foundationdb.ais.model.ForeignKey;
import com.foundationdb.ais.model.Group;
import com.foundationdb.ais.model.Join;
import com.foundationdb.ais.model.JoinColumn;
import com.foundationdb.ais.model.Table;
import com.foundationdb.util.ListUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/** Use join conditions to identify which tables are part of the same group.
*/
public class GroupJoinFinder extends BaseRule
{
private static final Logger logger = LoggerFactory.getLogger(GroupJoinFinder.class);
@Override
protected Logger getLogger() {
return logger;
}
@Override
public void apply(PlanContext plan) {
List<JoinIsland> islands = new JoinIslandFinder().find(plan.getPlan());
// this sets outerTables on all subqueries
new SubqueryBoundTablesTracker(plan) {}.run();
moveAndNormalizeWhereConditions(islands);
findGroupJoins(islands);
reorderJoins(islands);
isolateGroups(islands);
moveJoinConditions(islands);
}
static class JoinIslandFinder implements PlanVisitor, ExpressionVisitor {
private ColumnEquivalenceStack equivs = new ColumnEquivalenceStack();
List<JoinIsland> result = new ArrayList<>();
public List<JoinIsland> find(PlanNode root) {
root.accept(this);
return result;
}
@Override
public boolean visitEnter(PlanNode n) {
equivs.enterNode(n);
return visit(n);
}
@Override
public boolean visitLeave(PlanNode n) {
equivs.leaveNode(n);
return true;
}
@Override
public boolean visit(PlanNode n) {
if (n instanceof Joinable) {
Joinable joinable = (Joinable)n;
PlanWithInput output = joinable.getOutput();
if (!(output instanceof Joinable)) {
result.add(new JoinIsland(joinable, output, equivs.get(), equivs.getFks()));
}
}
return true;
}
@Override
public boolean visitEnter(ExpressionNode n) {
return visit(n);
}
@Override
public boolean visitLeave(ExpressionNode n) {
return true;
}
@Override
public boolean visit(ExpressionNode n) {
return true;
}
}
// A subtree of joins.
static class JoinIsland {
Joinable root;
PlanWithInput output;
ConditionList whereConditions;
List<TableGroupJoin> whereJoins;
EquivalenceFinder<ColumnExpression> columnEquivs;
EquivalenceFinder<ColumnExpression> fkEquivs;
public JoinIsland(Joinable root, PlanWithInput output, EquivalenceFinder<ColumnExpression> columnEquivs,
EquivalenceFinder<ColumnExpression> fkEquivs) {
this.root = root;
this.output = output;
this.columnEquivs = columnEquivs;
this.fkEquivs = fkEquivs;
if (output instanceof Select)
whereConditions = ((Select)output).getConditions();
}
public BaseQuery getQuery() {
PlanWithInput output = root.getOutput();
BaseQuery baseQuery = null;
while (output != null) {
if (output instanceof BaseQuery) {
baseQuery = (BaseQuery) output;
}
output = output.getOutput();
}
return baseQuery;
}
}
// First pass: find all the WHERE conditions above inner joins
// and put given join condition up there, since it's equivalent.
// While there, normalize comparisons.
protected void moveAndNormalizeWhereConditions(List<JoinIsland> islands) {
for (JoinIsland island : islands) {
if (island.whereConditions != null) {
moveInnerJoinConditions(island.root, island.whereConditions);
normalizeColumnComparisons(island.whereConditions);
}
normalizeColumnComparisons(island.root);
}
}
// So long as there are INNER joins, move their conditions up to
// the top-level join.
protected void moveInnerJoinConditions(Joinable joinable,
ConditionList whereConditions) {
if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
if (joinable.isInnerJoin()) {
ConditionList joinConditions = join.getJoinConditions();
if (joinConditions != null) {
whereConditions.addAll(joinConditions);
joinConditions.clear();
}
}
if (join.getJoinType() != JoinType.RIGHT) {
moveInnerJoinConditions(join.getLeft(), whereConditions);
}
if (join.getJoinType() != JoinType.LEFT) {
moveInnerJoinConditions(join.getRight(), whereConditions);
}
}
}
// Make comparisons involving a single column have
// the form <col> <op> <expr>, with the child on the left in the
// case of two columns, which is what we may then recognize as a
// group join.
protected void normalizeColumnComparisons(ConditionList conditions) {
if (conditions == null) return;
Collection<ConditionExpression> newExpressions = new ArrayList<>();
for (ConditionExpression cond : conditions) {
if (cond instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)cond;
ExpressionNode left = ccond.getLeft();
ExpressionNode right = ccond.getRight();
if (right.isColumn()) {
ColumnSource rightTable = ((ColumnExpression)right).getTable();
if (left.isColumn()) {
ColumnSource leftTable = ((ColumnExpression)left).getTable();
if (compareColumnSources(leftTable, rightTable) < 0) {
ccond.reverse();
}
}
else {
ccond.reverse();
}
}
}
}
conditions.addAll(newExpressions);
ListUtils.removeDuplicates(conditions);
}
// Normalize join's conditions and any below it.
protected void normalizeColumnComparisons(Joinable joinable) {
if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
normalizeColumnComparisons(join.getJoinConditions());
normalizeColumnComparisons(join.getLeft());
normalizeColumnComparisons(join.getRight());
}
}
// Third pass: put adjacent inner joined tables together in
// left-deep ascending-ordinal order. E.g. (CO)I.
protected void reorderJoins(List<JoinIsland> islands) {
for (JoinIsland island : islands) {
Joinable nroot = reorderJoins(island.root);
if (island.root != nroot) {
island.output.replaceInput(island.root, nroot);
island.root = nroot;
}
}
}
protected Joinable reorderJoins(Joinable joinable) {
if (countInnerJoins(joinable) > 1) {
List<Joinable> joins = new ArrayList<>();
getInnerJoins(joinable, joins);
for (int i = 0; i < joins.size(); i++) {
joins.set(i, reorderJoins(joins.get(i)));
}
return orderInnerJoins(joins);
}
else if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
join.setLeft(reorderJoins(join.getLeft()));
join.setRight(reorderJoins(join.getRight()));
if (compareJoinables(join.getLeft(), join.getRight()) > 0)
join.reverse();
}
return joinable;
}
// Make inner joins into a tree of group-tree / non-table.
protected Joinable orderInnerJoins(List<Joinable> joinables) {
Map<TableGroup,List<TableSource>> groups =
new HashMap<>();
List<Joinable> nonTables = new ArrayList<>();
for (Joinable joinable : joinables) {
if (joinable instanceof TableSource) {
TableSource table = (TableSource)joinable;
TableGroup group = table.getGroup();
List<TableSource> entry = groups.get(group);
if (entry == null) {
entry = new ArrayList<>();
groups.put(group, entry);
}
entry.add(table);
}
else
nonTables.add(joinable);
}
joinables.clear();
// Make order of groups predictable.
List<TableGroup> keys = new ArrayList<>(groups.keySet());
Collections.sort(keys, tableGroupComparator);
for (TableGroup gkey : keys) {
List<TableSource> group = groups.get(gkey);
Collections.sort(group, tableSourceComparator);
joinables.add(constructLeftInnerJoins(group));
}
joinables.addAll(nonTables);
if (joinables.size() > 1)
return constructRightInnerJoins(joinables);
else
return joinables.get(0);
}
// Group flattening is left-recursive.
protected Joinable constructLeftInnerJoins(List<? extends Joinable> joinables) {
Joinable result = joinables.get(0);
for (int i = 1; i < joinables.size(); i++) {
result = new JoinNode(result, joinables.get(i), JoinType.INNER);
}
return result;
}
// Nested loop joins are right-recursive.
protected Joinable constructRightInnerJoins(List<? extends Joinable> joinables) {
int size = joinables.size();
Joinable result = joinables.get(--size);
while (size > 0) {
result = new JoinNode(joinables.get(--size), result, JoinType.INNER);
// Reset the FK Join if we recreate the Join Tree
JoinNode node = (JoinNode) result;
if (node.getRight().isTable() &&
(((TableSource)node.getRight()).getParentFKJoin() != null)) {
node.setFKJoin(((TableSource)node.getRight()).getParentFKJoin());
}
if (node.getFKJoin() == null && node.getLeft().isTable() &&
(((TableSource)node.getLeft()).getParentFKJoin() != null)) {
node.setFKJoin(((TableSource)node.getLeft()).getParentFKJoin());
}
}
return result;
}
// Second pass: find join conditions corresponding to group joins.
protected void findGroupJoins(List<JoinIsland> islands) {
for (JoinIsland island : islands) {
List<TableGroupJoin> whereJoins = new ArrayList<>();
findGroupJoins(island.root, new ArrayDeque<JoinNode>(),
island.whereConditions, whereJoins,
island.columnEquivs);
island.whereJoins = whereJoins;
}
//Find FK Joins
for (JoinIsland island : islands) {
findFKGroups(island.root,
new ArrayDeque<JoinNode>(),
island.whereConditions,
island.columnEquivs,
island.fkEquivs);
}
// find single groups
for (JoinIsland island : islands) {
findSingleGroups(island.root);
}
}
protected void findGroupJoins(Joinable joinable,
Deque<JoinNode> outputJoins,
ConditionList whereConditions,
List<TableGroupJoin> whereJoins,
EquivalenceFinder<ColumnExpression> columnEquivs) {
if (joinable.isTable()) {
TableSource table = (TableSource)joinable;
for (JoinNode output : outputJoins) {
ConditionList conditions = output.getJoinConditions();
TableGroupJoin tableJoin = findParentJoin(table, conditions, columnEquivs);
if (tableJoin != null) {
output.setGroupJoin(tableJoin);
return;
}
}
TableGroupJoin tableJoin = findParentJoin(table, whereConditions, columnEquivs);
if (tableJoin != null) {
whereJoins.add(tableJoin); // Position after reordering.
return;
}
}
else if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
outputJoins.push(join);
if (join.isInnerJoin()) {
findGroupJoins(join.getLeft(), outputJoins, whereConditions, whereJoins, columnEquivs);
findGroupJoins(join.getRight(), outputJoins, whereConditions, whereJoins, columnEquivs);
}
else {
Deque<JoinNode> singleJoin = new ArrayDeque<>(1);
singleJoin.push(join);
// In a LEFT OUTER JOIN, the outer half is allowed to
// take from higher conditions.
if (join.getJoinType() == JoinType.LEFT)
findGroupJoins(join.getLeft(), outputJoins, whereConditions, whereJoins, columnEquivs);
else
findGroupJoins(join.getLeft(), singleJoin, null, null, columnEquivs);
if (join.getJoinType() == JoinType.RIGHT)
findGroupJoins(join.getRight(), outputJoins, whereConditions, whereJoins, columnEquivs);
else
findGroupJoins(join.getRight(), singleJoin, null, null, columnEquivs);
}
outputJoins.pop();
}
}
// Find a condition among the given conditions that matches the
// parent join for the given table.
protected TableGroupJoin findParentJoin(TableSource childTable,
ConditionList conditions,
EquivalenceFinder<ColumnExpression> columnEquivs) {
if ((conditions == null) || conditions.isEmpty()) return null;
TableNode childNode = childTable.getTable();
Join groupJoin = childNode.getTable().getParentJoin();
if (groupJoin == null) return null;
TableNode parentNode = childNode.getTree().getNode(groupJoin.getParent());
if (parentNode == null) return null;
List<JoinColumn> joinColumns = groupJoin.getJoinColumns();
int ncols = joinColumns.size();
Map<TableSource,GroupJoinConditions> parentTables = new HashMap<>();
for (int i = 0; i < ncols; ++i) {
if (!findGroupCondition(joinColumns, i, childTable, conditions, true, parentTables, columnEquivs)) {
if (!findGroupCondition(joinColumns, i, childTable, conditions, false, parentTables, columnEquivs)) {
return null; // join column had no direct or equivalent group joins, so we know the answer
}
}
}
TableSource parentTable = null;
GroupJoinConditions groupJoinConditions = null;
for (Map.Entry<TableSource,GroupJoinConditions> entry : parentTables.entrySet()) {
boolean found = true;
for (ComparisonCondition elem : entry.getValue().getConditions()) {
if (elem == null) {
found = false;
break;
}
}
if (found) {
if (parentTable == null) {
parentTable = entry.getKey();
groupJoinConditions = entry.getValue();
}
else {
// TODO: What we need is something
// earlier to decide that the primary
// keys are equated and so share the
// references somehow.
ConditionExpression c1 = groupJoinConditions.getConditions().get(0);
ConditionExpression c2 = entry.getValue().getConditions().get(0);
if (conditions.indexOf(c1) > conditions.indexOf(c2)) {
// Make the order predictable for tests.
ConditionExpression temp = c1;
c1 = c2;
c2 = temp;
}
throw new UnsupportedSQLException("Found two possible parent joins",
c2.getSQLsource());
}
}
}
if (parentTable == null) return null;
TableGroup group = parentTable.getGroup();
if (group == null) {
group = childTable.getGroup();
if (group == null)
group = new TableGroup(groupJoin.getGroup());
}
else if (childTable.getGroup() != null) {
group.merge(childTable.getGroup());
}
if (!tableAllowedInGroup(group, childTable))
return null;
groupJoinConditions.installGeneratedConditionsTo(conditions);
return new TableGroupJoin(group, parentTable, childTable, groupJoinConditions.getConditions(), groupJoin);
}
private boolean findGroupCondition(List<JoinColumn> joinColumns, int i, TableSource childTable,
ConditionList conditions, boolean requireExact,
Map<TableSource, GroupJoinConditions> parentTables,
EquivalenceFinder<ColumnExpression> columnEquivs)
{
int ncols = joinColumns.size();
boolean found = false;
for (ConditionExpression condition : conditions) {
List<ColumnExpression> colExprs = findColumnExpressions(condition);
if (!colExprs.isEmpty()) {
ComparisonCondition ccond = (ComparisonCondition)condition;
ComparisonCondition normalized = normalizedCond(
joinColumns.get(i).getChild(), joinColumns.get(i).getParent(),
childTable,
colExprs.get(0),
colExprs.get(1),
ccond,
requireExact,
columnEquivs
);
if (normalized != null) {
found = true;
ColumnExpression rnorm = (ColumnExpression) normalized.getRight();
TableSource parentSource = (TableSource) rnorm.getTable();
GroupJoinConditions entry = parentTables.get(parentSource);
if (entry == null) {
entry = new GroupJoinConditions(ncols);
parentTables.put(parentSource, entry);
}
entry.set(i, normalized, normalized != ccond);
}
}
}
return found;
}
private static class GroupJoinConditions {
private List<ComparisonCondition> conditions;
private List<ComparisonCondition> generatedConditions;
public GroupJoinConditions(int ncols) {
this.conditions = new ArrayList<>(Collections.<ComparisonCondition>nCopies(ncols, null));
}
public void set(int i, ComparisonCondition condition, boolean wasGenerated) {
conditions.set(i, condition);
if (wasGenerated) {
if (generatedConditions == null)
generatedConditions = new ArrayList<>(conditions.size());
generatedConditions.add(condition);
}
}
public List<ComparisonCondition> getConditions() {
return conditions;
}
public void installGeneratedConditionsTo(ConditionList conditionList) {
if (generatedConditions != null)
conditionList.addAll(generatedConditions);
}
}
private ComparisonCondition normalizedCond(Column childColumn, Column parentColumn,
TableSource childSource,
ColumnExpression lcol, ColumnExpression rcol,
ComparisonCondition originalCond, boolean requireExact,
EquivalenceFinder<ColumnExpression> columnEquivs)
{
// look for child
ColumnExpression childEquiv = null;
if (lcol.getTable() == childSource && lcol.getColumn() == childColumn) {
childEquiv = lcol;
}
else {
for (ColumnExpression equivalent : columnEquivs.findEquivalents(lcol)) {
if (equivalent.getTable() == childSource && equivalent.getColumn() == childColumn) {
childEquiv = equivalent;
break;
}
}
}
if (childEquiv == null)
return null;
// look for parent
ColumnExpression parentEquiv = null;
if (rcol.getColumn() == parentColumn) {
parentEquiv = rcol;
}
else {
for (ColumnExpression equivalent : columnEquivs.findEquivalents(rcol)) {
if (equivalent.getColumn() == parentColumn) {
parentEquiv = equivalent;
break;
}
}
}
if (parentEquiv == null)
return null;
boolean isExact = childEquiv == lcol && parentEquiv == rcol;
if (requireExact) {
return isExact ? originalCond : null;
}
else {
assert ! isExact : "exact match found; should have been discovered by previous invocation at call site";
return new ComparisonCondition(
Comparison.EQ,
childEquiv,
parentEquiv,
originalCond.getSQLtype(),
originalCond.getSQLsource(),
originalCond.getType()
);
}
}
protected boolean tableAllowedInGroup(TableGroup group, TableSource childTable) {
return true;
}
protected void findFKGroups(Joinable joinable,
Deque<JoinNode> outputJoins,
ConditionList whereConditions,
EquivalenceFinder<ColumnExpression> columnEquivs,
EquivalenceFinder<ColumnExpression> fkEquivs) {
if (joinable.isTable()) {
TableSource table = (TableSource)joinable;
for (JoinNode output : outputJoins) {
ConditionList conditions = output.getJoinConditions();
if (table.getGroup() == null) {
findParentFKJoin (table, conditions, columnEquivs,fkEquivs);
} else {
break;
}
}
if (table.getGroup() == null) {
findParentFKJoin (table, whereConditions, columnEquivs, fkEquivs);
}
} else if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
outputJoins.push(join);
findFKGroups (join.getLeft(), outputJoins, whereConditions, columnEquivs, fkEquivs);
findFKGroups (join.getRight(), outputJoins, whereConditions, columnEquivs, fkEquivs);
outputJoins.pop();
if (join.getRight().isTable() &&
(((TableSource)join.getRight()).getParentFKJoin() != null)) {
join.setFKJoin(((TableSource)join.getRight()).getParentFKJoin());
}
}
}
// Find a condition among the given conditions that matches the
// parent FK join for the given table.
protected void findParentFKJoin(TableSource childTable,
ConditionList conditions,
EquivalenceFinder<ColumnExpression> columnEquivs,
EquivalenceFinder<ColumnExpression> fkEquivs) {
if ((conditions == null) || conditions.isEmpty()) return;
TableNode childNode = childTable.getTable();
if (childNode.getTable().getForeignKeys() == null || childNode.getTable().getForeignKeys().isEmpty()) return;
for (ForeignKey key : childNode.getTable().getReferencingForeignKeys()) {
TableFKJoin join = findFKConditions(childTable, conditions, key, columnEquivs, fkEquivs);
if (join != null) {
if (childTable.getGroup() == null) {
childTable.setGroup(new TableGroup(childTable.getTable().getTable().getGroup()));
childTable.setParentFKJoin(join);
} else {
// TODO: If we find a child with 2 parents, don't mark this child as a
// FK Join. The JoinAndIndexPicker for the FK selection isn't
// (currently) smart enough to reorder the two joins correctly.
childTable.setParentFKJoin(null);
}
}
}
}
protected TableFKJoin findFKConditions (TableSource childSource,
ConditionList conditions,
ForeignKey key,
EquivalenceFinder<ColumnExpression> columnEquivs,
EquivalenceFinder<ColumnExpression> fkEquivs) {
List<ComparisonCondition> elements = new ArrayList<ComparisonCondition>(key.getReferencedColumns().size());
TableFKJoin join = null;
ColumnExpression parentEquiv = null;
List<JoinColumn> joinColumns = key.getJoinColumns();
for (int i = 0; i < joinColumns.size(); i++) {
for (ConditionExpression condition : conditions) {
List<ColumnExpression> columns = findColumnExpressions (condition);
if (!columns.isEmpty()) {
ComparisonCondition ccond = (ComparisonCondition)condition;
ComparisonCondition normalized = normalizedCond(
joinColumns.get(i).getChild(), joinColumns.get(i).getParent(),
childSource,
columns.get(0),
columns.get(1),
ccond,
true,
fkEquivs
);
if (normalized == null) {
normalized = normalizedCond(
joinColumns.get(i).getChild(), joinColumns.get(i).getParent(),
childSource,
columns.get(0),
columns.get(1),
ccond,
false,
fkEquivs
);
}
if (normalized != null) {
elements.add(normalized);
parentEquiv = (ColumnExpression)normalized.getRight();
break;
}
}
}
}
if (elements.size() == key.getReferencedColumns().size()) {
join = new TableFKJoin ((TableSource)parentEquiv.getTable(), childSource, elements, key);
}
return join;
}
protected List<ColumnExpression> findColumnExpressions(ConditionExpression condition) {
ArrayList<ColumnExpression> columns = new ArrayList<> (2);
if (condition instanceof ComparisonCondition) {
ComparisonCondition ccond = (ComparisonCondition)condition;
if (ccond.getOperation() == Comparison.EQ) {
ExpressionNode left = ccond.getLeft();
ExpressionNode right = ccond.getRight();
if (left.isColumn() && right.isColumn()) {
ColumnExpression lcol = (ColumnExpression)left;
ColumnExpression rcol = (ColumnExpression)right;
if ((lcol.getTable() instanceof TableSource) && (rcol.getTable() instanceof TableSource)) {
columns.add(lcol);
columns.add(rcol);
}
}
}
}
return columns;
}
protected void findSingleGroups(Joinable joinable) {
if (joinable.isTable()) {
TableSource table = (TableSource)joinable;
if (table.getGroup() == null) {
table.setGroup(new TableGroup(table.getTable().getTable().getGroup()));
}
}
else if (joinable.isJoin()) {
JoinNode join = (JoinNode)joinable;
findSingleGroups(join.getLeft());
findSingleGroups(join.getRight());
}
}
// Fourth pass: wrap contiguous group joins in separate joinable.
// We have done out best with the inner joins to make this possible,
// but some outer joins may require that a TableGroup be broken up into
// multiple TableJoins.
protected void isolateGroups(List<JoinIsland> islands) {
for (JoinIsland island : islands) {
TableGroupJoinNode tree = isolateGroupJoins(island.root);
if (tree != null) {
Joinable nroot = groupJoinTree(tree, island.root);
island.output.replaceInput(island.root, nroot);
island.root = nroot;
}
}
}
protected TableGroupJoinNode isolateGroupJoins(Joinable joinable) {
if (joinable.isTable()) {
TableSource table = (TableSource)joinable;
assert (table.getGroup() != null);
return new TableGroupJoinNode(table);
}
if (!joinable.isJoin())
return null;
JoinNode join = (JoinNode)joinable;
Joinable left = join.getLeft();
Joinable right = join.getRight();
TableGroupJoinNode leftTree = isolateGroupJoins(left);
TableGroupJoinNode rightTree = isolateGroupJoins(right);
if ((leftTree != null) && (rightTree != null) &&
// All tables below the two sides must be from the same group.
(leftTree.getTable().getGroup() == rightTree.getTable().getGroup())) {
// An outer join condition must be one that can be
// done before flattening because after that it's too
// late to get back the outer side if the test never
// succeeds.
boolean joinOK;
switch (join.getJoinType()) {
case INNER:
joinOK = true;
break;
case LEFT:
joinOK = checkJoinConditions(join.getJoinConditions(), leftTree, rightTree);
break;
case RIGHT:
// Cannot allow any non-group conditions, since even
// one only on parent would kill the child because
// that's how Select_HKeyOrdered works.
joinOK = checkJoinConditions(join.getJoinConditions(), null, leftTree);
break;
default:
joinOK = false;
}
if (joinOK) {
// Still need to be able to splice them together.
TableGroupJoinNode tree;
int leftDepth = leftTree.getTable().getTable().getDepth();
int rightDepth = rightTree.getTable().getTable().getDepth();
if (leftDepth < rightDepth)
tree = spliceGroupJoins(leftTree, rightTree, join.getJoinType());
else if (rightDepth < leftDepth)
tree = spliceGroupJoins(rightTree, leftTree, join.getJoinType());
else
tree = null;
if (tree != null) {
return tree;
}
}
}
// Did not manage to coalesce. Put in any intermediate trees.
if (leftTree != null)
join.setLeft(groupJoinTree(leftTree, left));
if (rightTree != null)
join.setRight(groupJoinTree(rightTree, right));
// Make arbitrary joins LEFT not RIGHT.
if (join.getJoinType() == JoinType.RIGHT)
join.reverse();
return null;
}
protected TableGroupJoinTree groupJoinTree(TableGroupJoinNode root, Joinable joins) {
TableGroupJoinTree tree = new TableGroupJoinTree(root);
Set<TableSource> required = new HashSet<>();
getRequiredTables(joins, required);
tree.setRequired(required);
for (TableGroupJoinNode node : root) {
node.getTable().setOutput(tree); // Instead of join that is going away.
}
return tree;
}
// Get all the tables reachable via inner joins from here.
protected void getRequiredTables(Joinable joinable, Set<TableSource> required) {
if (joinable instanceof TableSource) {
required.add((TableSource)joinable);
}
else if (joinable instanceof JoinNode) {
JoinNode join = (JoinNode)joinable;
if (join.getJoinType() != JoinType.RIGHT)
getRequiredTables(join.getLeft(), required);
if (join.getJoinType() != JoinType.LEFT)
getRequiredTables(join.getRight(), required);
}
}
// Combine trees at the proper branch point.
protected TableGroupJoinNode spliceGroupJoins(TableGroupJoinNode parent,
TableGroupJoinNode child,
JoinType joinType) {
TableGroupJoinNode branch = parent.findTable(child.getTable().getParentTable());
if (branch == null)
return null;
child.setParent(branch);
child.setParentJoinType(joinType);
TableGroupJoinNode prev = null;
while (true) {
TableGroupJoinNode next = (prev == null) ? branch.getFirstChild() : prev.getNextSibling();
if ((next == null) ||
(next.getTable().getTable().getOrdinal() > child.getTable().getTable().getOrdinal())) {
child.setNextSibling(next);
if (prev == null)
branch.setFirstChild(child);
else
prev.setNextSibling(child);
break;
}
prev = next;
}
return parent;
}
protected boolean checkJoinConditions(ConditionList joinConditions,
TableGroupJoinNode outer,
TableGroupJoinNode inner) {
if (hasIllegalReferences(joinConditions, outer))
return false;
inner.setJoinConditions(joinConditions);
return true;
}
// See whether any expression in the join condition other than the
// grouping join references a table under the given tree.
protected boolean hasIllegalReferences(ConditionList joinConditions,
TableGroupJoinNode fromTree) {
JoinedReferenceFinder finder = null;
if (joinConditions != null) {
for (ConditionExpression condition : joinConditions) {
if (condition.getImplementation() == ConditionExpression.Implementation.GROUP_JOIN)
continue; // Group condition okay.
if (fromTree == null)
return true; // All non-group disallowed.
if (finder == null)
finder = new JoinedReferenceFinder(fromTree);
if (finder.find(condition))
return true; // Has references to other side.
}
}
return false;
}
static class JoinedReferenceFinder implements PlanVisitor, ExpressionVisitor {
private TableGroupJoinNode fromTree;
private boolean found;
public JoinedReferenceFinder(TableGroupJoinNode fromTree) {
this.fromTree = fromTree;
}
public boolean find(ExpressionNode expression) {
found = false;
expression.accept(this);
return found;
}
@Override
public boolean visitEnter(PlanNode n) {
return visit(n);
}
@Override
public boolean visitLeave(PlanNode n) {
return true;
}
@Override
public boolean visit(PlanNode n) {
return true;
}
@Override
public boolean visitEnter(ExpressionNode n) {
return visit(n);
}
@Override
public boolean visitLeave(ExpressionNode n) {
return true;
}
@Override
public boolean visit(ExpressionNode n) {
if (n instanceof ColumnExpression) {
ColumnSource table = ((ColumnExpression)n).getTable();
if (table instanceof TableSource) {
if (fromTree.findTable((TableSource)table) != null) {
found = true;
}
}
}
return true;
}
}
// Fifth pass: move the WHERE conditions back to their actual
// joins, which may be different from the ones they were on in the
// original query and reject any group joins that now cross TableJoins.
protected void moveJoinConditions(List<JoinIsland> islands) {
for (JoinIsland island : islands) {
moveJoinConditions(island.root, island.whereConditions, island.whereJoins);
if (island.whereConditions != null) {
Iterator<ConditionExpression> iterator = island.whereConditions.iterator();
while (iterator.hasNext()) {
ConditionExpression condition = iterator.next();
Set<ColumnSource> columnSources = new ConditionColumnSourcesFinder().find(condition);
columnSources.removeAll(island.getQuery().getOuterTables());
if (moveWhereCondition(columnSources, condition, island.root)) {
iterator.remove();
}
}
}
}
}
protected void moveJoinConditions(Joinable joinable,
ConditionList whereConditions, List<TableGroupJoin> whereJoins) {
if (joinable instanceof TableGroupJoinTree) {
for (TableGroupJoinNode table : (TableGroupJoinTree)joinable) {
TableGroupJoin tableJoin = table.getTable().getParentJoin();
if (tableJoin != null) {
if (table.getParent() == null) {
tableJoin.reject(); // Did not make it into the group.
}
else if (whereJoins.contains(tableJoin)) {
List<ComparisonCondition> joinConditions = tableJoin.getConditions();
// Move down from WHERE conditions to join conditions.
if (table.getJoinConditions() == null)
table.setJoinConditions(new ConditionList());
table.getJoinConditions().addAll(joinConditions);
whereConditions.removeAll(joinConditions);
}
}
}
}
else if (joinable instanceof JoinNode) {
JoinNode join = (JoinNode)joinable;
join.setGroupJoin(null);
moveJoinConditions(join.getLeft(), whereConditions, whereJoins);
moveJoinConditions(join.getRight(), whereConditions, whereJoins);
}
}
/**
* Moves the given condition as far down as possible, so long as the tableSources are visible
* @param tableSources the tableSources referenced by the condition. All sources that are declared in a nested
* join will be removed from the tableSources
* @return true if the condition was added to a joinConditions
*/
private boolean moveWhereCondition(Set<ColumnSource> tableSources, ConditionExpression condition, Joinable joinable) {
// If we move Any/Exists Conditions down, the InConditionReverser won't be able to find them,
// so they get to stay in the where clause
if (condition instanceof AnyCondition || condition instanceof ExistsCondition) {
return false;
}
if (joinable instanceof TableGroupJoinTree) {
for (TableGroupJoinNode table : (TableGroupJoinTree)joinable) {
tableSources.remove(table.getTable());
}
} else if (joinable instanceof JoinNode)
{
JoinNode join = (JoinNode)joinable;
if (join.isInnerJoin()) {
Set<ColumnSource> forLeft = new HashSet<>(tableSources);
Set<ColumnSource> forRight = new HashSet<>(tableSources);
if (moveWhereCondition(forLeft, condition, join.getLeft())) {
return true;
}
if (forLeft.isEmpty()) {
tableSources.clear();
} else {
if (moveWhereCondition(forRight, condition, join.getRight())) {
return true;
}
tableSources.retainAll(forLeft);
tableSources.retainAll(forRight);
}
if (tableSources.isEmpty()) {
if (join.getJoinConditions() == null) {
join.setJoinConditions(new ConditionList());
}
join.getJoinConditions().add(condition);
return true;
}
}
return false;
} else if (joinable instanceof TableSource)
{
tableSources.remove(joinable);
return false;
}
return false;
}
static final Comparator<TableGroup> tableGroupComparator = new Comparator<TableGroup>() {
@Override
public int compare(TableGroup tg1, TableGroup tg2) {
Group g1 = tg1.getGroup();
Group g2 = tg2.getGroup();
if (g1 != g2)
return g1.getName().compareTo(g2.getName());
int o1 = tg1.getMinOrdinal();
int o2 = tg2.getMinOrdinal();
if (o1 == o2) {
TableSource ts1 = tg1.findByOrdinal(o1);
TableSource ts2 = tg2.findByOrdinal(o2);
return ts1.getName().compareTo(ts2.getName());
}
return o1 - o2;
}
};
static final Comparator<TableSource> tableSourceComparator = new Comparator<TableSource>() {
public int compare(TableSource t1, TableSource t2) {
return compareTableSources(t1, t2);
}
};
protected static int compareColumnSources(ColumnSource c1, ColumnSource c2) {
if (c1 instanceof TableSource) {
if (!(c2 instanceof TableSource))
return +1;
return compareTableSources((TableSource)c1, (TableSource)c2);
}
else if (c2 instanceof TableSource)
return -1;
else
return 0;
}
protected static int compareTableSources(TableSource ts1, TableSource ts2) {
TableNode t1 = ts1.getTable();
Table ut1 = t1.getTable();
Group g1 = ut1.getGroup();
TableGroup tg1 = ts1.getGroup();
TableNode t2 = ts2.getTable();
Table ut2 = t2.getTable();
Group g2 = ut2.getGroup();
TableGroup tg2 = ts2.getGroup();
if (g1 != g2)
return g1.getName().compareTo(g2.getName());
if (tg1 == tg2) { // Including null because not yet computed.
if (ut1 == ut2)
return ts1.getName().compareTo(ts2.getName());
return t1.getOrdinal() - t2.getOrdinal();
}
return tg1.getMinOrdinal() - tg2.getMinOrdinal();
}
// Return size of directly-reachable subtree of all simple inner joins.
protected static int countInnerJoins(Joinable joinable) {
if (!isSimpleInnerJoin(joinable))
return 0;
return 1 +
countInnerJoins(((JoinNode)joinable).getLeft()) +
countInnerJoins(((JoinNode)joinable).getRight());
}
// Accumulate operands of directly-reachable subtree of simple inner joins.
protected static void getInnerJoins(Joinable joinable, Collection<Joinable> into) {
if (!isSimpleInnerJoin(joinable))
into.add(joinable);
else {
getInnerJoins(((JoinNode)joinable).getLeft(), into);
getInnerJoins(((JoinNode)joinable).getRight(), into);
}
}
// Can this inner join be reorderd?
// TODO: If there are inner joins with conditions that didn't get
// moved by the first pass, leave them alone. That will miss
// opportunities. Need to have a way to accumulate those
// conditions and put them into the join tree.
protected static boolean isSimpleInnerJoin(Joinable joinable) {
return (joinable.isInnerJoin() && !((JoinNode)joinable).hasJoinConditions());
}
protected static int compareJoinables(Joinable j1, Joinable j2) {
if (j1.isTable() && j2.isTable())
return compareTableSources((TableSource)j1, (TableSource)j2);
Group g1 = singleGroup(j1);
Group g2 = singleGroup(j2);
if (g1 == null) {
if (g2 != null)
return -1;
else
return 0;
}
else if (g2 == null)
return +1;
if (g1 != g2)
return g1.getName().compareTo(g2.getName());
int[] range1 = ordinalRange(j1);
int[] range2 = ordinalRange(j2);
if (range1[1] < range2[0])
return -1;
else if (range1[0] > range2[1])
return +1;
else
return 0;
}
protected static Group singleGroup(Joinable j) {
if (j.isTable())
return ((TableSource)j).getTable().getGroup();
else if (j.isJoin()) {
JoinNode join = (JoinNode)j;
Group gl = singleGroup(join.getLeft());
Group gr = singleGroup(join.getRight());
if (gl == gr)
return gl;
else
return null;
}
else
return null;
}
protected static int[] ordinalRange(Joinable j) {
if (j.isTable()) {
int ord = ((TableSource)j).getTable().getOrdinal();
return new int[] { ord, ord };
}
else if (j.isJoin()) {
JoinNode join = (JoinNode)j;
int[] ol = ordinalRange(join.getLeft());
int[] or = ordinalRange(join.getRight());
if (ol[0] > or[0])
ol[0] = or[0];
if (ol[1] < or[1])
ol[1] = or[1];
return ol;
}
else
return new int[] { -1, -1 };
}
}