package com.tesora.dve.sql.transform.strategy.joinsimplification; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * 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/>. * #L% */ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import com.tesora.dve.common.MultiMap; import com.tesora.dve.exceptions.PEException; import com.tesora.dve.sql.expression.ColumnKey; import com.tesora.dve.sql.expression.ExpressionUtils; import com.tesora.dve.sql.expression.TableKey; import com.tesora.dve.sql.node.Edge; import com.tesora.dve.sql.node.LanguageNode; import com.tesora.dve.sql.node.expression.ColumnInstance; import com.tesora.dve.sql.node.expression.ExpressionNode; import com.tesora.dve.sql.node.expression.FunctionCall; import com.tesora.dve.sql.node.expression.TableInstance; import com.tesora.dve.sql.node.structural.FromTableReference; import com.tesora.dve.sql.node.structural.JoinedTable; import com.tesora.dve.sql.node.test.EngineConstant; import com.tesora.dve.sql.schema.SchemaContext; import com.tesora.dve.sql.statement.dml.DMLStatement; import com.tesora.dve.sql.util.ListOfPairs; import com.tesora.dve.sql.util.Pair; public class MultijoinSimplifier extends Simplifier { @Override public boolean applies(SchemaContext sc, DMLStatement dmls) throws PEException { return EngineConstant.FROMCLAUSE.has(dmls); } @Override public DMLStatement simplify(SchemaContext sc,DMLStatement in, JoinSimplificationTransformFactory parent) throws PEException { List<LanguageNode> explicitJoins = EngineConstant.FROMCLAUSE.getMulti(in); String before = (parent.emitting() ? in.getSQL(sc) : null); // suppose we have something of the form // A a inner join B b on a.id=b.id (any kind) join C c on b.sid=c.sid and c.id=a.id // we seek to turn the nway join at the end (C is joined to both A & B) into a regular binary join by rewriting // a.id to be b.id (which is legal because of the inner join). This rewrite always has to be applied left to right. boolean any = false; for(LanguageNode ln : explicitJoins) { if (ln instanceof FromTableReference) { FromTableReference ftr = (FromTableReference) ln; if (processTableChain(ftr, sc)) { ftr.getBlock().clear(); any = true; } } } if (any) { if (parent.emitting()) { parent.emit(" MJS in: " + before); parent.emit(" MJS out: " + in.getSQL(sc)); } return in; } return null; } private static void accTable(Map<TableKey,Integer> into, TableKey tk) { Integer already = into.get(tk); if (already == null) into.put(tk, new Integer(1)); else into.put(tk, new Integer(already.intValue() + 1)); } private static void decTable(Map<TableKey,Integer> into, TableKey tk) { Integer already = into.get(tk); if (already == null) return; if (already.intValue() == 1) { into.remove(tk); } else { into.put(tk, new Integer(already.intValue() - 1)); } } private static boolean processTableChain(FromTableReference ftr, final SchemaContext sc) { MultiMap<ColumnKey,ColumnKey> equivs = new MultiMap<ColumnKey,ColumnKey>(); boolean any = false; for(JoinedTable jt : ftr.getTableJoins()) { HashMap<TableKey,Integer> tabs = new HashMap<TableKey,Integer>(); ListOfPairs<ColumnInstance,ColumnInstance> equijoins = new ListOfPairs<ColumnInstance,ColumnInstance>(); ExpressionNode joinEx = jt.getJoinOn(); List<ExpressionNode> decompAnd = ExpressionUtils.decomposeAndClause(joinEx); for(ExpressionNode en : decompAnd) { if (Boolean.TRUE.equals(EngineConstant.EQUIJOIN.getValue(en, sc))) { FunctionCall fc = (FunctionCall) en; ExpressionNode lhn = fc.getParametersEdge().get(0); ExpressionNode rhn = fc.getParametersEdge().get(1); if (lhn instanceof ColumnInstance && rhn instanceof ColumnInstance) { ColumnInstance lhs = (ColumnInstance) lhn; ColumnInstance rhs = (ColumnInstance) rhn; equijoins.add(lhs, rhs); accTable(tabs, lhs.getTableInstance().getTableKey()); accTable(tabs, rhs.getTableInstance().getTableKey()); } } } if (jt.getJoinType().isInnerJoin()) { for(Pair<ColumnInstance,ColumnInstance> p : equijoins) { ColumnKey lk = p.getFirst().getColumnKey(); ColumnKey rk = p.getSecond().getColumnKey(); equivs.put(lk, rk); equivs.put(rk, lk); } } if (tabs.size() > 2) { TableInstance target = jt.getJoinedToTable(); TableKey targetTK = target.getTableKey(); tabs.remove(targetTK); // nway join. // we can't change the target, but we can change the nontarget side MultiMap<Pair<ColumnInstance,ColumnInstance>,ColumnKey> modCandidates = new MultiMap<Pair<ColumnInstance,ColumnInstance>,ColumnKey>(); for(Pair<ColumnInstance,ColumnInstance> p : equijoins) { ColumnInstance toModify = p.getFirst().getTableInstance() == target ? p.getSecond() : p.getFirst(); Collection<ColumnKey> eq = equivs.get(toModify.getColumnKey()); if (eq == null || eq.isEmpty()) continue; // only select column keys whose tab keys are in the keyset List<ColumnKey> candidates = new ArrayList<ColumnKey>(); for(ColumnKey ck : eq) { if (tabs.containsKey(ck.getTableKey())) { candidates.add(ck); } } if (candidates.isEmpty()) continue; for(ColumnKey ck : candidates) { modCandidates.put(p,ck); } } if (modCandidates.isEmpty()) continue; // now for all the repls in modCandidates, choose the best table. The best table is the one that is in all of the entries. LinkedHashSet<TableKey> intersection = null; for(Pair<ColumnInstance,ColumnInstance> p : modCandidates.keySet()) { LinkedHashSet<TableKey> ctks = new LinkedHashSet<TableKey>(); for(ColumnKey ck : modCandidates.get(p)) ctks.add(ck.getTableKey()); if (intersection == null) intersection = ctks; else intersection.retainAll(ctks); } if (intersection.isEmpty()) continue; // we're just going to arbitrarily choose the first one TableKey candidateTable = intersection.iterator().next(); // set up replacement values - but don't make the change yet HashMap<ColumnInstance,ColumnInstance> replacements = new HashMap<ColumnInstance,ColumnInstance>(); for(Pair<ColumnInstance,ColumnInstance> p : modCandidates.keySet()) { LinkedHashSet<ColumnKey> replCandidates = new LinkedHashSet<ColumnKey>(modCandidates.get(p)); for(Iterator<ColumnKey> riter = replCandidates.iterator(); riter.hasNext();) { if (riter.next().getTableKey().equals(candidateTable)) { // leave it alone } else { riter.remove(); } } if (replCandidates.size() != 1) { continue; } ColumnKey replacementColumn = replCandidates.iterator().next(); if (p.getFirst().getTableInstance() == target) { replacements.put(p.getSecond(),replacementColumn.toInstance()); decTable(tabs,p.getSecond().getTableInstance().getTableKey()); } else { replacements.put(p.getFirst(), replacementColumn.toInstance()); decTable(tabs,p.getFirst().getTableInstance().getTableKey()); } } if (tabs.size() == 1) { // success! make the changes for(Map.Entry<ColumnInstance, ColumnInstance> me : replacements.entrySet()) { Edge<?,ExpressionNode> parentEdge = me.getKey().getParentEdge(); parentEdge.set(me.getValue()); } joinEx.getBlock().clear(); any = true; } } } return any; } }