/* * Copyright 2006 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.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.Storage; import com.amazon.carbonado.SupportException; import com.amazon.carbonado.filter.Filter; import com.amazon.carbonado.filter.PropertyFilter; import com.amazon.carbonado.filter.RelOp; import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.info.StorableProperty; /** * Analyzes a simple query specification and determines which index is best * suited for its execution. Query filters passed to this analyzer cannot * contain any 'or' operations. * *
IndexedQueryAnalyzer is sharable and thread-safe. An instance for a
 * particular Storable type can be cached, avoiding repeated construction
 * cost. In addition, the analyzer caches learned foreign indexes.
 *
 * @author Brian S O'Neill
 * @see UnionQueryAnalyzer
 */
public class IndexedQueryAnalyzer {
    final Class mType;
    final RepositoryAccess mRepoAccess;
    // Growable cache which maps join properties to lists of usable foreign indexes.
    private Map> mForeignIndexCache;
    /**
     * @param type type of storable being queried
     * @param access repository access for examing available indexes
     * @throws IllegalArgumentException if type or indexProvider is null
     */
    public IndexedQueryAnalyzer(Class type, RepositoryAccess access) {
        if (type == null || access == null) {
            throw new IllegalArgumentException();
        }
        mType = type;
        mRepoAccess = access;
    }
    public Class getStorableType() {
        return mType;
    }
    /**
     * @param filter optional filter which which must be {@link Filter#isBound
     * bound} and cannot contain any logical 'or' operations.
     * @param ordering optional properties which define desired ordering
     * @param hints optional query hints
     * @throws IllegalArgumentException if filter is not supported
     */
    public Result analyze(Filter filter, OrderingList ordering, QueryHints hints)
        throws SupportException, RepositoryException
    {
        if (filter != null && !filter.isBound()) {
            throw new IllegalArgumentException("Filter must be bound");
        }
        // First find best local index.
        CompositeScore bestLocalScore = null;
        StorableIndex bestLocalIndex = null;
        final Comparator index : localIndexes) {
                CompositeScore candidateScore =
                    CompositeScore.evaluate(index, filter, ordering);
                if (bestLocalScore == null
                    || fullComparator.compare(candidateScore, bestLocalScore) < 0)
                {
                    bestLocalScore = candidateScore;
                    bestLocalIndex = index;
                }
            }
        }
        // Now try to find best foreign index.
        if (bestLocalScore != null && bestLocalScore.getFilteringScore().isKeyMatch()) {
            // Don't bother checking foreign indexes. The local one is perfect.
            return new Result(filter, bestLocalScore, bestLocalIndex, null, null, hints);
        }
        CompositeScore> bestForeignScore = null;
        StorableIndex> bestForeignIndex = null;
        ChainedProperty bestForeignProperty = null;
        for (PropertyFilter propFilter : PropertyFilterList.get(filter)) {
            ChainedProperty chainedProp = propFilter.getChainedProperty();
            if (chainedProp.getChainCount() == 0) {
                // Cannot possibly be a join, so move on.
                continue;
            }
            ForeignIndexes foreignIndexes = getForeignIndexes(chainedProp);
            if (foreignIndexes == null) {
                continue;
            }
            for (StorableIndex> index : foreignIndexes.mIndexes) {
                CompositeScore candidateScore = CompositeScore.evaluate
                    (foreignIndexes.getVirtualIndex(index),
                     index.isUnique(),
                     index.isClustered(),
                     filter,
                     ordering);
                if (bestForeignScore == null
                    || fullComparator.compare(candidateScore, bestForeignScore) < 0)
                {
                    bestForeignScore = candidateScore;
                    bestForeignIndex = index;
                    bestForeignProperty = foreignIndexes.mProperty;
                }
            }
        }
        // Check if foreign index is better than local index.
        if (bestLocalScore != null && bestForeignScore != null) {
            // When comparing local index to foreign index, use a slightly less
            // discriminating comparator, to prevent foreign indexes from
            // looking too good.
            Comparator getForeignIndexes(ChainedProperty chainedProp)
        throws SupportException, RepositoryException
    {
        // Remove the last property as it is expected to be a simple storable
        // property instead of a joined Storable.
        chainedProp = chainedProp.trim();
        ForeignIndexes foreignIndexes = null;
        if (mForeignIndexCache != null) {
            foreignIndexes = mForeignIndexCache.get(chainedProp);
            if (foreignIndexes != null || mForeignIndexCache.containsKey(chainedProp)) {
                return foreignIndexes;
            }
        }
        // Check if property chain is properly joined and indexed along the way.
        evaluate: {
            if (!isProperJoin(chainedProp.getPrimeProperty())) {
                break evaluate;
            }
            if (chainedProp.isOuterJoin(0)) {
                // Outer joins cannot be optimized via foreign indexes.
                break evaluate;
            }
            int count = chainedProp.getChainCount();
            for (int i=0; i(chainedProp, indexes);
        }
        if (mForeignIndexCache == null) {
            mForeignIndexCache = Collections.synchronizedMap
                (new HashMap>());
        }
        mForeignIndexCache.put(chainedProp, foreignIndexes);
        return foreignIndexes;
    }
    /**
     * Checks if the property is a join and its internal properties are fully
     * indexed.
     */
    private boolean isProperJoin(StorableProperty> property)
        throws SupportException, RepositoryException
    {
        if (!property.isJoin() || property.isQuery()) {
            return false;
        }
        // Make up a filter over the join's internal properties and then search
        // for an index that filters with no remainder.
        Filter> filter = Filter.getOpenFilter(property.getEnclosingType());
        int count = property.getJoinElementCount();
        for (int i=0; i mFilter;
        private final CompositeScore mScore;
        private final StorableIndex mLocalIndex;
        private final StorableIndex> mForeignIndex;
        private final ChainedProperty mForeignProperty;
        private final QueryHints mHints;
        Result(Filter filter,
               CompositeScore score,
               StorableIndex localIndex,
               StorableIndex> foreignIndex,
               ChainedProperty foreignProperty,
               QueryHints hints)
        {
            mFilter = filter;
            mScore = score;
            mLocalIndex = localIndex;
            mForeignIndex = foreignIndex;
            mForeignProperty = foreignProperty;
            mHints = hints;
        }
        /**
         * Returns true if the selected index does anything at all to filter
         * results or to order them. If not, a filtered and sorted full scan
         * makes more sense.
         */
        public boolean handlesAnything() {
            return mScore.getFilteringScore().hasAnyMatches() == true
                || mScore.getOrderingScore().getHandledCount() > 0;
        }
        /**
         * Returns combined handled and remainder filter for this result.
         */
        public Filter getFilter() {
            return mFilter;
        }
        /**
         * Returns combined handled and remainder orderings for this result.
         */
        public OrderingList getOrdering() {
            return mScore.getOrderingScore().getHandledOrdering().concat(getRemainderOrdering());
        }
        /**
         * Returns the score on how well the selected index performs the
         * desired filtering and ordering.
         */
        public CompositeScore getCompositeScore() {
            return mScore;
        }
        /**
         * Remainder filter which overrides that in composite score.
         */
        public Filter getRemainderFilter() {
            return mScore.getFilteringScore().getRemainderFilter();
        }
        /**
         * Remainder orderings which override that in composite score.
         */
        public OrderingList getRemainderOrdering() {
            return mScore.getOrderingScore().getRemainderOrdering();
        }
        /**
         * Returns the local index that was selected, or null if a foreign
         * index was selected.
         */
        public StorableIndex getLocalIndex() {
            return mLocalIndex;
        }
        /**
         * Returns the foreign index that was selected, or null if a local
         * index was selected. If a foreign index has been selected, then a
         * {@link JoinedQueryExecutor} is needed.
         */
        public StorableIndex> getForeignIndex() {
            return mForeignIndex;
        }
        /**
         * Returns the simple or chained property that maps to the selected
         * foreign index. Returns null if foreign index was not selected. This
         * property corresponds to the "targetToSourceProperty" of {@link
         * JoinedQueryExecutor}.
         */
        public ChainedProperty getForeignProperty() {
            return mForeignProperty;
        }
        /**
         * Returns true if local or foreign index is clustered. Scans of
         * clustered indexes are generally faster.
         */
        public boolean isIndexClustered() {
            return (mLocalIndex != null && mLocalIndex.isClustered())
                || (mForeignIndex != null && mForeignIndex.isClustered());
        }
        /**
         * Returns true if the given result uses the same index as this, and in
         * the same way. The only allowed differences are in the remainder
         * filter and orderings.
         */
        public boolean canMergeRemainder(Result other) {
            if (this == other || (!handlesAnything() && !other.handlesAnything())) {
                return true;
            }
            if (equals(getLocalIndex(), other.getLocalIndex())
                && equals(getForeignIndex(), other.getForeignIndex())
                && equals(getForeignProperty(), other.getForeignProperty()))
            {
                return getCompositeScore().canMergeRemainder(other.getCompositeScore());
            }
            return false;
        }
        /**
         * Merges the remainder filter and orderings of this result with the
         * one given, returning a new result. Call canMergeRemainder first to
         * verify if the merge makes any sense.
         */
        public Result mergeRemainder(Result other) {
            if (this == other) {
                return this;
            }
            // Assuming canMergeRemainder returned true, each handled filter
            // and the combined filter are all identical. This is just a safeguard.
            Filter handledFilter =
                orFilters(getCompositeScore().getFilteringScore().getHandledFilter(),
                          other.getCompositeScore().getFilteringScore().getHandledFilter());
            Filter remainderFilter =
                orFilters(getRemainderFilter(), other.getRemainderFilter());
            OrderingList remainderOrdering =
                getRemainderOrdering().concat(other.getRemainderOrdering()).reduce();
            Filter filter = andFilters(handledFilter, remainderFilter);
            CompositeScore score = mScore
                .withRemainderFilter(remainderFilter)
                .withRemainderOrdering(remainderOrdering);
            return new Result(filter, score, mLocalIndex, mForeignIndex, mForeignProperty, mHints);
        }
        /**
         * Merges the remainder filter of this result with the given filter,
         * returning a new result. If handlesAnything return true, then it
         * doesn't usually make sense to call this method.
         */
        public Result mergeRemainderFilter(Filter filter) {
            return withRemainderFilter(orFilters(getRemainderFilter(), filter));
        }
        private Filter andFilters(Filter a, Filter b) {
            if (a == null) {
                return b;
            }
            if (b == null) {
                return a;
            }
            return a.and(b).reduce();
        }
        private Filter orFilters(Filter a, Filter b) {
            if (a == null) {
                return b;
            }
            if (b == null) {
                return a;
            }
            return a.or(b).reduce();
        }
        /**
         * Returns a new result with the remainder filter replaced.
         */
        public Result withRemainderFilter(Filter remainderFilter) {
            Filter handledFilter = getCompositeScore().getFilteringScore().getHandledFilter();
            Filter filter = andFilters(handledFilter, remainderFilter);
            CompositeScore score = mScore.withRemainderFilter(remainderFilter);
            return new Result(filter, score, mLocalIndex, mForeignIndex, mForeignProperty, mHints);
        }
        /**
         * Returns a new result with the remainder ordering replaced.
         */
        public Result withRemainderOrdering(OrderingList remainderOrdering) {
            CompositeScore score = mScore.withRemainderOrdering(remainderOrdering);
            return new Result(mFilter, score, mLocalIndex,
                              mForeignIndex, mForeignProperty, mHints);
        }
        /**
         * Creates a QueryExecutor based on this result.
         */
        public QueryExecutor createExecutor()
            throws SupportException, FetchException, RepositoryException
        {
            StorableIndex localIndex = getLocalIndex();
            StorageAccess localAccess = mRepoAccess.storageAccessFor(getStorableType());
            if (localIndex != null) {
                Storage delegate = localAccess.storageDelegate(localIndex);
                if (delegate != null) {
                    return new DelegatedQueryExecutor(delegate, getFilter(), getOrdering());
                }
            }
            Filter remainderFilter = getRemainderFilter();
            QueryExecutor executor;
            if (!handlesAnything()) {
                executor = new FullScanQueryExecutor(localAccess);
            } else if (localIndex == null) {
                // Use foreign executor.
                return JoinedQueryExecutor.build
                    (mRepoAccess, getForeignProperty(), getFilter(), getOrdering(), mHints);
            } else {
                CompositeScore score = getCompositeScore();
                FilteringScore fScore = score.getFilteringScore();
                if (fScore.isKeyMatch()) {
                    executor = new KeyQueryExecutor(localAccess, localIndex, fScore);
                } else {
                    IndexedQueryExecutor ixExecutor =
                        new IndexedQueryExecutor(localAccess, localIndex, score);
                    executor = ixExecutor;
                    if (ixExecutor.getCoveringFilter() != null) {
                        remainderFilter = fScore.getCoveringRemainderFilter();
                    }
                }
            }
            if (remainderFilter != null) {
                executor = new FilteredQueryExecutor(executor, remainderFilter);
            }
            OrderingList remainderOrdering = getRemainderOrdering();
            if (remainderOrdering.size() > 0) {
                executor = new SortedQueryExecutor
                    (localAccess,
                     executor,
                     getCompositeScore().getOrderingScore().getHandledOrdering(),
                     remainderOrdering);
            }
            return executor;
        }
        @Override
        public String toString() {
            return "IndexedQueryAnalyzer.Result {score="
                + getCompositeScore() + ", localIndex="
                + getLocalIndex() + ", foreignIndex="
                + getForeignIndex() + ", foreignProperty="
                + getForeignProperty() + ", remainderFilter="
                + getRemainderFilter() + ", remainderOrdering="
                + getRemainderOrdering() + '}';
        }
        private boolean equals(Object a, Object b) {
            return a == null ? (b == null) : (a.equals(b));
        }
    }
    private static class ForeignIndexes {
        final ChainedProperty mProperty;
        final List[]> mVirtualIndexMap;
        /**
         * @param property type of property must be a joined Storable
         */
        ForeignIndexes(ChainedProperty property, Collection[]>();
        }
        /**
         * Prepends local chained property with names of index elements,
         * producing a virtual index on local storable. This allows
         * CompositeScore to evaluate it.
         */
        synchronized OrderedProperty[] getVirtualIndex(StorableIndex> index) {
            OrderedProperty[] virtualProps = mVirtualIndexMap.get(index);
            if (virtualProps != null) {
                return virtualProps;
            }
            OrderedProperty>[] realProps = index.getOrderedProperties();
            virtualProps = new OrderedProperty[realProps.length];
            for (int i=realProps.length; --i>=0; ) {
                OrderedProperty> realProp = realProps[i];
                ChainedProperty virtualChained =
                    mProperty.append(realProp.getChainedProperty());
                virtualProps[i] = OrderedProperty.get(virtualChained, realProp.getDirection());
            }
            mVirtualIndexMap.put(index, virtualProps);
            return virtualProps;
        }
    }
}