/** * 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.util.ArgumentValidation; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; public class EquivalenceFinder<T> { public void copyEquivalences(EquivalenceFinder<? extends T> source) { equivalences.putAll(source.equivalences); } public void markEquivalent(T one, T two) { ArgumentValidation.notNull("first arg", one); ArgumentValidation.notNull("second arg", two); if (one.equals(two)) return; // equals implies equivalence even without this equivalences.put(one, two); equivalences.put(two, one); } public boolean areEquivalent(T one, T two) { initSeenItems(); return areEquivalent(one, two, maxTraversals); } public Set<T> findEquivalents(T node) { Set<T> accumulator = new HashSet<>(); buildEquivalents(node, accumulator); boolean removedFirst = accumulator.remove(node); assert removedFirst : "didn't remove " + node + " from " + accumulator; return accumulator; } /** * <p>Return a set of all of the equivalence pairs defined in this instance, with reflected-duplicates removed. * For instance, rather than a pair <code>(u, v)</code> and another pair <code>(v, u)</code>, this set will contain * either <code>(u, v)</code> <em>or</em> <code>(v, u)</code>, but not both.</p> * * <p>The resulting set is returned as a <code>Map.Entry</code>, but "key" and "value" are arbitrary in this * context.</p> * @return the equivalence entries */ public Set<Entry<T, T>> equivalencePairs() { Collection<Entry<T, T>> entries = equivalences.entries(); Map<T,T> normalized = new HashMap<>(entries.size() / 2); for (Entry<T,T> entry : entries) { T key = entry.getKey(); T val = entry.getValue(); boolean sawReflection; if (normalized.containsKey(val)) { T otherKey = normalized.get(val); if (otherKey == null) sawReflection = (key == null); else sawReflection = otherKey.equals(key); } else { sawReflection = false; } if (!sawReflection) normalized.put(key, val); } return normalized.entrySet(); } public Set<T> findParticipants() { Set<Entry<T,T>> pairs = equivalencePairs(); Set<T> results = new HashSet<>(pairs.size() * 2); for (Entry<T,T> pair : pairs) { results.add(pair.getKey()); results.add(pair.getValue()); } return results; } @Override public String toString() { Set<Entry<T, T>> normalizedEntries = equivalencePairs(); StringBuilder sb = new StringBuilder(EquivalenceFinder.class.getSimpleName()).append('{'); for (Iterator<Entry<T, T>> iterator = normalizedEntries.iterator(); iterator.hasNext(); ) { Entry<T, T> entry = iterator.next(); String first = elementToString(entry.getKey()); String second = elementToString(entry.getValue()); sb.append('(').append(first).append(" = ").append(second).append(')'); if (iterator.hasNext()) sb.append(", "); } return sb.append('}').toString(); } protected String describeNull() { return "null"; } protected String describeElement(T element) { return element.toString(); } // for testing void tooMuchTraversing() { } private String elementToString(T element) { return element == null ? describeNull() : describeElement(element); } private boolean areEquivalent(T one, T two, int remainingTraversals) { // base case if (one.equals(two)) return true; // limit on search space if (--remainingTraversals < 0) { tooMuchTraversing(); return false; } // we've seen this edge before, so don't need to traverse it again if (!seenNodes.add(one)) return false; // recurse Collection<T> oneEquivalents = equivalences.get(one); // we do two quick-pass checks which take advantage of the fact that the collection is a Set with quick lookups if (oneEquivalents.isEmpty()) return false; if (oneEquivalents.contains(two)) return true; for (T equivalent : oneEquivalents) { if (areEquivalent(equivalent, two, remainingTraversals)) return true; } return false; } private void buildEquivalents(T node, Set<T> accumulator) { if (!accumulator.add(node)) return; for (T equivalence : equivalences.get(node)) { buildEquivalents(equivalence, accumulator); } } private void initSeenItems() { if (seenNodes == null) seenNodes = new HashSet<>(); else seenNodes.clear(); } public EquivalenceFinder() { this(32); } EquivalenceFinder(int maxTraversals) { this.maxTraversals = maxTraversals; } /** * Mapping of equivalences, one node at a time. To save a bit of space (both in puts and reads), for any two * equivalent nodes n1 and n2, if they have different hashCodes, then we only put in the key n1 with value n2. * If they have the same hash code, we have no option but to put them both in. */ private Multimap<T,T> equivalences = HashMultimap.create(); private int maxTraversals; private Set<T> seenNodes; }