/* * Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates. * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks * of Amazon Technologies, Inc. or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.amazon.carbonado.qe; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import com.amazon.carbonado.Storable; import com.amazon.carbonado.info.Direction; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableKey; import com.amazon.carbonado.info.StorableProperty; /** * Manages a set of {@link StorableIndex} objects, intended for reducing the * set such that the minimal amount of physical indexes need to be defined for * a specific type of {@link Storable}. * * @author Brian S O'Neill */ public class StorableIndexSet<S extends Storable> extends TreeSet<StorableIndex<S>> { private static final long serialVersionUID = -5840661016235340456L; private static final Comparator<StorableIndex<?>> STORABLE_INDEX_COMPARATOR = new StorableIndexComparator(); public StorableIndexSet() { super(STORABLE_INDEX_COMPARATOR); } /** * Copy constructor. */ public StorableIndexSet(StorableIndexSet<S> set) { super(STORABLE_INDEX_COMPARATOR); addAll(set); } /** * Adds all the indexes of the given storable. * * @throws IllegalArgumentException if info is null */ public void addIndexes(StorableInfo<S> info) { for (int i=info.getIndexCount(); --i>=0; ) { add(info.getIndex(i)); } } /** * Adds all the indexes of the given storable. * * @param defaultDirection default ordering direction to apply to each * index property * @throws IllegalArgumentException if any argument is null */ public void addIndexes(StorableInfo<S> info, Direction defaultDirection) { for (int i=info.getIndexCount(); --i>=0; ) { add(info.getIndex(i).setDefaultDirection(defaultDirection)); } } /** * Adds all of the alternate keys of the given storable as indexes by * calling {@link #addKey addKey}. * * @throws IllegalArgumentException if info is null */ public void addAlternateKeys(StorableInfo<S> info) { if (info == null) { throw new IllegalArgumentException(); } for (int i=info.getAlternateKeyCount(); --i>=0; ) { addKey(info.getAlternateKey(i)); } } /** * Adds the primary key of the given storable as indexes by calling {@link * #addKey addKey}. This method should not be called if the primary key * cannot be altered because persistent data is already stored against * it. Instead, the primary key index should be added as a normal index. * * <p>After adding the primary key via this method and after reducing the * set, call {@link #findPrimaryKeyIndex findPrimaryKeyIndex} to get the * best index to represent the primary key. * * @throws IllegalArgumentException if info is null */ public void addPrimaryKey(StorableInfo<S> info) { if (info == null) { throw new IllegalArgumentException(); } addKey(info.getPrimaryKey()); } /** * Adds the key as a unique index, preserving the property arrangement. * * @throws IllegalArgumentException if key is null */ @SuppressWarnings("unchecked") public void addKey(StorableKey<S> key) { if (key == null) { throw new IllegalArgumentException(); } add(new StorableIndex<S>(key, Direction.UNSPECIFIED)); } /** * Reduces the size of the set by removing redundant indexes, and merges * others together. */ public void reduce() { reduce(Direction.UNSPECIFIED); } /** * Reduces the size of the set by removing redundant indexes, and merges * others together. * * @param defaultDirection replace unspecified property directions with this */ public void reduce(Direction defaultDirection) { List<StorableIndex<S>> group = new ArrayList<StorableIndex<S>>(); Map<StorableIndex<S>, StorableIndex<S>> mergedReplacements = new TreeMap<StorableIndex<S>, StorableIndex<S>>(STORABLE_INDEX_COMPARATOR); Iterator<StorableIndex<S>> it = iterator(); while (it.hasNext()) { StorableIndex<S> candidate = it.next(); if (group.size() == 0 || isDifferentGroup(group.get(0), candidate)) { group.clear(); group.add(candidate); continue; } if (isRedundant(group, candidate, mergedReplacements)) { it.remove(); } else { group.add(candidate); } } // Now replace merged indexes. replaceEntries(mergedReplacements); setDefaultDirection(defaultDirection); } /** * Set the default direction for all index properties. * * @param defaultDirection replace unspecified property directions with this */ public void setDefaultDirection(Direction defaultDirection) { // Apply default sort direction to those unspecified. if (defaultDirection != Direction.UNSPECIFIED) { Map<StorableIndex<S>, StorableIndex<S>> replacements = null; for (StorableIndex<S> index : this) { StorableIndex<S> replacement = index.setDefaultDirection(defaultDirection); if (replacement != index) { if (replacements == null) { replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>(); } replacements.put(index, replacement); } } replaceEntries(replacements); } } /** * Marks all indexes as clustered or non-clustered. * * @param clustered true to mark clustered; false to mark non-clustered * @see StorableIndex#isClustered() * @since 1.2 */ public void markClustered(boolean clustered) { Map<StorableIndex<S>, StorableIndex<S>> replacements = null; for (StorableIndex<S> index : this) { StorableIndex<S> replacement = index.clustered(clustered); if (replacement != index) { if (replacements == null) { replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>(); } replacements.put(index, replacement); } } replaceEntries(replacements); } /** * Augment non-unique indexes with primary key properties, thus making them * unique. * * @throws IllegalArgumentException if info is null */ public void uniquify(StorableInfo<S> info) { if (info == null) { throw new IllegalArgumentException(); } uniquify(info.getPrimaryKey()); } /** * Augment non-unique indexes with key properties, thus making them unique. * * @throws IllegalArgumentException if key is null */ public void uniquify(StorableKey<S> key) { if (key == null) { throw new IllegalArgumentException(); } // Replace indexes which were are implied unique, even if they are not // declared as such. { Map<StorableIndex<S>, StorableIndex<S>> replacements = null; for (StorableIndex<S> index : this) { if (!index.isUnique() && isUniqueImplied(index)) { if (replacements == null) { replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>(); } replacements.put(index, index.unique(true)); } } replaceEntries(replacements); } // Now augment with key properties. { Map<StorableIndex<S>, StorableIndex<S>> replacements = null; for (StorableIndex<S> index : this) { StorableIndex<S> replacement = index.uniquify(key); if (replacement != index) { if (replacements == null) { replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>(); } replacements.put(index, replacement); } } replaceEntries(replacements); } } /** * Finds the best index to represent the primary key. Should be called * after calling reduce. As long as the primary key was added via {@link * #addPrimaryKey addPrimaryKey}, this method should never return null. * * @throws IllegalArgumentException if info is null */ public StorableIndex<S> findPrimaryKeyIndex(StorableInfo<S> info) { if (info == null) { throw new IllegalArgumentException(); } return findKeyIndex(info.getPrimaryKey()); } /** * Finds the best index to represent the given key. Should be called after * calling reduce. As long as the key was added via {@link #addKey addKey}, * this method should never return null. * * @throws IllegalArgumentException if key is null */ public StorableIndex<S> findKeyIndex(StorableKey<S> key) { if (key == null) { throw new IllegalArgumentException(); } Set<? extends OrderedProperty<S>> orderedProps = key.getProperties(); Set<StorableProperty<S>> keyProps = new HashSet<StorableProperty<S>>(); for (OrderedProperty<S> orderedProp : orderedProps) { keyProps.add(orderedProp.getChainedProperty().getPrimeProperty()); } search: for (StorableIndex<S> index : this) { if (!index.isUnique() || index.getPropertyCount() != keyProps.size()) { continue search; } for (int i=index.getPropertyCount(); --i>=0; ) { if (!keyProps.contains(index.getProperty(i))) { continue search; } } return index; } return null; } /** * Return true if index is unique or fully contains the members of a unique index. */ private boolean isUniqueImplied(StorableIndex<S> candidate) { if (candidate.isUnique()) { return true; } if (this.size() <= 1) { return false; } Set<StorableProperty<S>> candidateProps = new HashSet<StorableProperty<S>>(); for (int i=candidate.getPropertyCount(); --i>=0; ) { candidateProps.add(candidate.getProperty(i)); } search: for (StorableIndex<S> index : this) { if (!index.isUnique()) { continue search; } for (int i=index.getPropertyCount(); --i>=0; ) { if (!candidateProps.contains(index.getProperty(i))) { continue search; } } return true; } return false; } private boolean isDifferentGroup(StorableIndex<S> groupLeader, StorableIndex<S> candidate) { int count = candidate.getPropertyCount(); if (count > groupLeader.getPropertyCount()) { return true; } for (int i=0; i<count; i++) { StorableProperty aProp = groupLeader.getProperty(i); StorableProperty bProp = candidate.getProperty(i); if (aProp.getName().compareTo(bProp.getName()) != 0) { return true; } } return candidate.isUnique() && (count < groupLeader.getPropertyCount()); } /** * Returns true if candidate index is less qualified than an existing group * member, or if it was merged with another group member. If it was merged, * then an entry is placed in the merged map, and the given group list is * updated. */ private boolean isRedundant(List<StorableIndex<S>> group, StorableIndex<S> candidate, Map<StorableIndex<S>, StorableIndex<S>> mergedReplacements) { // All visited group members will have an equal or greater number of // properties. This is ensured by the ordering of the set. int count = candidate.getPropertyCount(); ListIterator<StorableIndex<S>> it = group.listIterator(); groupScan: while (it.hasNext()) { StorableIndex<S> member = it.next(); boolean moreQualified = false; boolean canReverse = true; boolean reverse = false; for (int i=0; i<count; i++) { Direction candidateOrder = candidate.getPropertyDirection(i); if (candidateOrder == Direction.UNSPECIFIED) { // Property direction is unspecified, so no need to compare // direction. Move on to next property. continue; } Direction memberOrder = member.getPropertyDirection(i); if (memberOrder == Direction.UNSPECIFIED) { // Candidate index is more qualified because member // property under examination hasn't specified a // direction. Move on to next property to continue checking // if a merge is possible. moreQualified = true; continue; } if (reverse) { candidateOrder = candidateOrder.reverse(); } if (candidateOrder == memberOrder) { // Direction exactly matches, move on to next property. canReverse = false; continue; } // If this point is reached, then the direction would match if // one was reversed. For an index to fully match, all // properties must be reversed. if (canReverse) { // Switch to reverse mode and move on to next property. reverse = true; canReverse = false; continue; } // Match failed and merge is not possible. continue groupScan; } if (moreQualified) { // Candidate is more qualified than all members compared to so // far, but it can be merged. Once merged, it is redundant. Direction[] directions = member.getPropertyDirections(); for (int i=0; i<count; i++) { if (directions[i] == Direction.UNSPECIFIED) { Direction direction = candidate.getPropertyDirection(i); directions[i] = reverse ? direction.reverse() : direction; } } StorableIndex<S> merged = new StorableIndex<S>(member.getProperties(), directions) .unique(member.isUnique()); mergedReplacements.put(member, merged); it.set(merged); } // Candidate is redundant. return true; } return false; } private void replaceEntries(Map<StorableIndex<S>, StorableIndex<S>> replacements) { if (replacements != null) { for (Map.Entry<StorableIndex<S>, StorableIndex<S>> e : replacements.entrySet()) { remove(e.getKey()); add(e.getValue()); } } } /** * Orders indexes such that they are grouped by property names. Within * those groups, indexes are ordered most qualified to least qualified. */ private static class StorableIndexComparator implements Comparator<StorableIndex<?>>, java.io.Serializable { private static final long serialVersionUID = 2204885249683067349L; public int compare(StorableIndex<?> a, StorableIndex<?> b) { if (a == b) { return 0; } int aCount = a.getPropertyCount(); int bCount = b.getPropertyCount(); int count = Math.min(aCount, bCount); for (int i=0; i<count; i++) { StorableProperty aProp = a.getProperty(i); StorableProperty bProp = b.getProperty(i); int result = aProp.getName().compareTo(bProp.getName()); if (aProp.getName().compareTo(bProp.getName()) != 0) { return result; } } // Index with more properties is first. if (aCount > bCount) { return -1; } else if (aCount < bCount) { return 1; } // Counts are the same, property names are the same. Unique indexes // are first, followed by index with more leading directions. Favor // ascending direction. for (int i=0; i<count; i++) { if (a.isUnique()) { if (!b.isUnique()) { return -1; } } else if (b.isUnique()) { return 1; } Direction aDirection = a.getPropertyDirection(i); Direction bDirection = b.getPropertyDirection(i); if (aDirection == bDirection) { continue; } // These order in which these tests are performed must not be // altered without careful examination. if (aDirection == Direction.ASCENDING) { return -1; } if (bDirection == Direction.ASCENDING) { return 1; } if (aDirection == Direction.DESCENDING) { return -1; } if (bDirection == Direction.DESCENDING) { return 1; } } return 0; } } }