/** * 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.ais.model.Column; import com.foundationdb.ais.model.ForeignKey; import com.foundationdb.ais.model.Index; import com.foundationdb.ais.model.IndexColumn; import com.foundationdb.ais.model.Table; import com.foundationdb.ais.model.TableIndex; import com.foundationdb.server.types.texpressions.Comparison; import com.foundationdb.sql.optimizer.plan.BaseQuery; import com.foundationdb.sql.optimizer.plan.ColumnExpression; import com.foundationdb.sql.optimizer.plan.ComparisonCondition; import com.foundationdb.sql.optimizer.plan.ConditionExpression; import com.foundationdb.sql.optimizer.plan.ExpressionNode; import com.foundationdb.sql.optimizer.plan.ExpressionVisitor; import com.foundationdb.sql.optimizer.plan.JoinNode; import com.foundationdb.sql.optimizer.plan.Joinable; import com.foundationdb.sql.optimizer.plan.PlanNode; import com.foundationdb.sql.optimizer.plan.PlanVisitor; import com.foundationdb.sql.optimizer.plan.Select; import com.foundationdb.sql.optimizer.plan.TableNode; import com.foundationdb.sql.optimizer.plan.TableSource; import com.foundationdb.sql.optimizer.plan.TableTree; import com.foundationdb.sql.optimizer.rule.JoinAndIndexPicker.JoinEnumerator; import com.foundationdb.sql.optimizer.rule.JoinAndIndexPicker.JoinsFinder; import com.foundationdb.sql.optimizer.rule.JoinAndIndexPicker.Picker; import com.foundationdb.sql.types.DataTypeDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; public final class ColumnEquivalenceFinder extends BaseRule { private static final Logger logger = LoggerFactory.getLogger(ColumnEquivalenceFinder.class); @Override protected Logger getLogger() { return logger; } private static class ColumnEquivalenceVisitor implements PlanVisitor, ExpressionVisitor { private ColumnEquivalenceStack equivs = new ColumnEquivalenceStack(); @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 JoinNode) { JoinNode joinNode = (JoinNode)n; if (joinNode.isInnerJoin()) equivalenceConditions(joinNode.getJoinConditions()); } else if (n instanceof Select) { Select select = (Select) n; equivalenceConditions(select.getConditions()); } return true; } private void equivalenceConditions(List<ConditionExpression> conditions) { if (conditions != null) { for (ConditionExpression condition : conditions) equivalenceCondition(condition); } } @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; } private void equivalenceCondition(ConditionExpression condition) { if (condition instanceof ComparisonCondition) { ComparisonCondition comparison = (ComparisonCondition) condition; if (comparison.getOperation().equals(Comparison.EQ) && (comparison.getLeft() instanceof ColumnExpression) && (comparison.getRight() instanceof ColumnExpression) ) { ColumnExpression left = (ColumnExpression) comparison.getLeft(); ColumnExpression right = (ColumnExpression) comparison.getRight(); if (!left.equals(right)) { markNotNull(left); markNotNull(right); equivs.get().markEquivalent(left, right); // also implies right.equivalentTo(left) } } } } } private static void markNotNull(ColumnExpression columnExpression) { DataTypeDescriptor sqLtype = columnExpression.getSQLtype(); if (sqLtype.isNullable()) { DataTypeDescriptor notNullable = sqLtype.getNullabilityType(false); columnExpression.setSQLtype(notNullable); } } @Override public void apply(PlanContext plan) { // Do basic column equivalence finding plan.getPlan().accept(new ColumnEquivalenceVisitor()); // Do FK table equivalence finding // Loop through all the tables, and find any possible FK Parents // Add these to the possible Equivalences BaseQuery basePlan = (BaseQuery)(plan.getPlan()); List<Picker> pickers = new JoinsFinder(plan).find(); for (Picker picker : pickers) { addFKEquivsFromJoins (picker.rootJoin(), picker.query.getFKEquivalencies()); picker.query.getFKEquivalencies().copyEquivalences(picker.query.getColumnEquivalencies()); } } private void addFKEquivsFromJoins(Joinable joins, EquivalenceFinder<ColumnExpression> equivalencies) { List<Joinable> tables = new ArrayList<>(); JoinEnumerator.addTables(joins, tables); Map<Table, TableSource> sources = new HashMap<>(); for (Joinable table: tables) { if (table instanceof TableSource) { TableSource tableSource = (TableSource)table; sources.put(tableSource.getTable().getTable(), tableSource); } } for (Joinable table: tables) { if (table instanceof TableSource) { TableSource tableSource = (TableSource) table; checkFKParents (tableSource.getTable().getTable(), tableSource, sources, equivalencies); } } } private void checkFKParents(Table child, TableSource tableSource, Map<Table, TableSource> sources, EquivalenceFinder<ColumnExpression> equivelances) { for (ForeignKey key : child.getReferencingForeignKeys()) { if (checkParentFKIsPK (key, child)) { TableSource parentSource = sources.get(key.getReferencedTable()); if (parentSource == null) { parentSource = generateTableSource(key.getReferencedTable()); sources.put(key.getReferencedTable(), parentSource); } checkFKParents (key.getReferencedTable(), parentSource, sources, equivelances); for (int i = 0; i < key.getReferencedColumns().size(); i++) { ColumnExpression one = expressionFromColumn(key.getReferencingColumns().get(i), tableSource); ColumnExpression two = expressionFromColumn(key.getReferencedColumns().get(i), parentSource); equivelances.markEquivalent(one, two); } } } } private boolean checkParentFKIsPK (ForeignKey key, Table child) { if (key.getReferencingTable().equals(child) && // TODO: This is temporary redundant. key.getReferencedIndex().isPrimaryKey()) { // Check the child table FK columns are the child table PK as well. // TODO: We may be able to relax this with further testing. List<Column> fkColumns = key.getReferencingColumns(); TableIndex childPKIndex = key.getReferencingTable().getIndex(Index.PRIMARY); // this can occur if the child table has no declared primary key. if (childPKIndex == null) { return false; } List<IndexColumn> pkColumns = childPKIndex.getKeyColumns(); if (fkColumns.size() != pkColumns.size()) return false; for (int i = 0; i < fkColumns.size(); i++) { if (!fkColumns.get(i).equals(pkColumns.get(i).getColumn())) { return false; } } return true; } return false; } private TableSource generateTableSource (Table table) { return new TableSource (new TableNode (table, new TableTree()), true, table.getName().toString()); } private ColumnExpression expressionFromColumn(Column col, TableSource source) { if (source == null) { Table table = col.getTable(); TableNode node = new TableNode(table, new TableTree()); source = new TableSource(node, true, table.getName().toString()); } return new ColumnExpression(source, col); } }