/*
* 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.io.IOException;
import java.util.List;
import com.amazon.carbonado.Cursor;
import com.amazon.carbonado.FetchException;
import com.amazon.carbonado.Query;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.filter.Filter;
import com.amazon.carbonado.filter.FilterValues;
import com.amazon.carbonado.filter.PropertyFilter;
import com.amazon.carbonado.filter.RelOp;
import com.amazon.carbonado.info.Direction;
import com.amazon.carbonado.info.StorableIndex;
/**
* QueryExecutor which utilizes an index.
*
* @author Brian S O'Neill
*/
public class IndexedQueryExecutor<S extends Storable> extends AbstractQueryExecutor<S> {
/**
* Compares two objects which are assumed to be Comparable. If one value is
* null, it is treated as being higher. This consistent with all other
* property value comparisons in carbonado.
*/
static int compareWithNullHigh(Object a, Object b) {
return a == null ? (b == null ? 0 : -1) : (b == null ? 1 : ((Comparable) a).compareTo(b));
}
private final Support<S> mSupport;
private final StorableIndex<S> mIndex;
private final int mHandledCount;
private final int mIdentityCount;
private final Filter<S> mIdentityFilter;
private final List<PropertyFilter<S>> mExclusiveRangeStartFilters;
private final List<PropertyFilter<S>> mInclusiveRangeStartFilters;
private final List<PropertyFilter<S>> mExclusiveRangeEndFilters;
private final List<PropertyFilter<S>> mInclusiveRangeEndFilters;
private final boolean mReverseOrder;
private final boolean mReverseRange;
private final Filter<S> mCoveringFilter;
// Total of nine start and end boundary type permutations.
private final Query<?>[] mIndexEntryQueryCache;
/**
* @param index index to use, which may be a primary key index
* @param score score determines how best to utilize the index
* @throws IllegalArgumentException if any parameter is null
*/
public IndexedQueryExecutor(Support<S> support,
StorableIndex<S> index,
CompositeScore<S> score)
throws FetchException
{
if (support == null && this instanceof Support) {
support = (Support<S>) this;
}
if (support == null || index == null || score == null) {
throw new IllegalArgumentException();
}
mSupport = support;
mIndex = index;
FilteringScore<S> fScore = score.getFilteringScore();
OrderingScore<S> oScore = score.getOrderingScore();
mHandledCount = oScore.getHandledCount();
mIdentityCount = fScore.getIdentityCount();
mIdentityFilter = fScore.getIdentityFilter();
mExclusiveRangeStartFilters = fScore.getExclusiveRangeStartFilters();
mInclusiveRangeStartFilters = fScore.getInclusiveRangeStartFilters();
mExclusiveRangeEndFilters = fScore.getExclusiveRangeEndFilters();
mInclusiveRangeEndFilters = fScore.getInclusiveRangeEndFilters();
mReverseOrder = oScore.shouldReverseOrder();
mReverseRange = fScore.shouldReverseRange();
Query<?> indexEntryQuery = support.indexEntryQuery(index);
if (indexEntryQuery == null) {
mCoveringFilter = null;
mIndexEntryQueryCache = null;
} else {
mCoveringFilter = fScore.getCoveringFilter();
mIndexEntryQueryCache = new Query[9]; // Nine start and end boundary permutations
}
}
@Override
public Class<S> getStorableType() {
// Storable type of filter may differ if index is used along with a
// join. The type of the index is the correct storable type.
return mIndex.getStorableType();
}
public Cursor<S> fetch(FilterValues<S> values) throws FetchException {
return fetch(values, null);
}
public Cursor<S> fetch(FilterValues<S> values, Query.Controller controller)
throws FetchException
{
Object[] identityValues = null;
Object rangeStartValue = null;
Object rangeEndValue = null;
BoundaryType rangeStartBoundary = BoundaryType.OPEN;
BoundaryType rangeEndBoundary = BoundaryType.OPEN;
if (values != null) {
if (mIdentityFilter != null) {
identityValues = values.getValuesFor(mIdentityFilter);
}
// In determining the proper range values and boundary types, the
// order in which this code runs is important. The exclusive
// filters must be checked before the inclusive filters.
for (int i=mExclusiveRangeStartFilters.size(); --i>=0; ) {
Object value = values.getValue(mExclusiveRangeStartFilters.get(i));
if (rangeStartBoundary == BoundaryType.OPEN ||
compareWithNullHigh(value, rangeStartValue) > 0)
{
rangeStartValue = value;
rangeStartBoundary = BoundaryType.EXCLUSIVE;
}
}
for (int i=mInclusiveRangeStartFilters.size(); --i>=0; ) {
Object value = values.getValue(mInclusiveRangeStartFilters.get(i));
if (rangeStartBoundary == BoundaryType.OPEN ||
compareWithNullHigh(value, rangeStartValue) > 0)
{
rangeStartValue = value;
rangeStartBoundary = BoundaryType.INCLUSIVE;
}
}
for (int i=mExclusiveRangeEndFilters.size(); --i>=0; ) {
Object value = values.getValue(mExclusiveRangeEndFilters.get(i));
if (rangeEndBoundary == BoundaryType.OPEN ||
compareWithNullHigh(value, rangeEndValue) < 0)
{
rangeEndValue = value;
rangeEndBoundary = BoundaryType.EXCLUSIVE;
}
}
for (int i=mInclusiveRangeEndFilters.size(); --i>=0; ) {
Object value = values.getValue(mInclusiveRangeEndFilters.get(i));
if (rangeEndBoundary == BoundaryType.OPEN ||
compareWithNullHigh(value, rangeEndValue) < 0)
{
rangeEndValue = value;
rangeEndBoundary = BoundaryType.INCLUSIVE;
}
}
}
Query<?> indexEntryQuery = getIndexEntryQuery(rangeStartBoundary, rangeEndBoundary);
if (indexEntryQuery == null) {
return mSupport.fetchSubset(mIndex, identityValues,
rangeStartBoundary, rangeStartValue,
rangeEndBoundary, rangeEndValue,
mReverseRange,
mReverseOrder,
controller);
} else {
indexEntryQuery = indexEntryQuery.withValues(identityValues);
if (rangeStartBoundary != BoundaryType.OPEN) {
indexEntryQuery = indexEntryQuery.with(rangeStartValue);
}
if (rangeEndBoundary != BoundaryType.OPEN) {
indexEntryQuery = indexEntryQuery.with(rangeEndValue);
}
if (mCoveringFilter != null && values != null) {
indexEntryQuery = indexEntryQuery.withValues(values.getValuesFor(mCoveringFilter));
}
return mSupport.fetchFromIndexEntryQuery(mIndex, indexEntryQuery, controller);
}
}
/**
* @return null if executor doesn't support or use a covering index
*/
public Filter<S> getCoveringFilter() {
return mCoveringFilter;
}
public Filter<S> getFilter() {
Filter<S> filter = mIdentityFilter;
for (PropertyFilter<S> p : mExclusiveRangeStartFilters) {
filter = filter == null ? p : filter.and(p);
}
for (PropertyFilter<S> p : mInclusiveRangeStartFilters) {
filter = filter == null ? p : filter.and(p);
}
for (PropertyFilter<S> p : mExclusiveRangeEndFilters) {
filter = filter == null ? p : filter.and(p);
}
for (PropertyFilter<S> p : mInclusiveRangeEndFilters) {
filter = filter == null ? p : filter.and(p);
}
if (mCoveringFilter != null) {
filter = filter == null ? mCoveringFilter : filter.and(mCoveringFilter);
}
if (filter == null) {
return Filter.getOpenFilter(getStorableType());
}
return filter;
}
public OrderingList<S> getOrdering() {
OrderingList<S> list;
if (mHandledCount == 0) {
list = OrderingList.emptyList();
} else {
list = OrderingList.get(mIndex.getOrderedProperties());
if (mIdentityCount > 0) {
list = list.subList(mIdentityCount, list.size());
}
if (mHandledCount < list.size()) {
list = list.subList(0, mHandledCount);
}
if (mReverseOrder) {
list = list.reverseDirections();
}
}
return list;
}
public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> values)
throws IOException
{
indent(app, indentLevel);
if (mReverseOrder) {
app.append("reverse ");
}
if (mIndex.isClustered()) {
app.append("clustered ");
}
app.append("index scan: ");
app.append(mIndex.getStorableType().getName());
newline(app);
indent(app, indentLevel);
app.append("...index: ");
mIndex.appendTo(app);
newline(app);
if (mIdentityFilter != null) {
indent(app, indentLevel);
app.append("...identity filter: ");
mIdentityFilter.appendTo(app, values);
newline(app);
}
if (mInclusiveRangeStartFilters.size() > 0 || mExclusiveRangeStartFilters.size() > 0 ||
mInclusiveRangeEndFilters.size() > 0 || mExclusiveRangeEndFilters.size() > 0)
{
indent(app, indentLevel);
app.append("...range filter: ");
int count = 0;
for (PropertyFilter<S> p : mExclusiveRangeStartFilters) {
if (count++ > 0) {
app.append(" & ");
}
p.appendTo(app, values);
}
for (PropertyFilter<S> p : mInclusiveRangeStartFilters) {
if (count++ > 0) {
app.append(" & ");
}
p.appendTo(app, values);
}
for (PropertyFilter<S> p : mExclusiveRangeEndFilters) {
if (count++ > 0) {
app.append(" & ");
}
p.appendTo(app, values);
}
for (PropertyFilter<S> p : mInclusiveRangeEndFilters) {
if (count++ > 0) {
app.append(" & ");
}
p.appendTo(app, values);
}
newline(app);
}
if (mCoveringFilter != null) {
indent(app, indentLevel);
app.append("...covering filter: ");
mCoveringFilter.appendTo(app, values);
newline(app);
}
return true;
}
/**
* @return null if query not supported
*/
private Query<?> getIndexEntryQuery(BoundaryType rangeStartBoundary,
BoundaryType rangeEndBoundary)
throws FetchException
{
Query<?>[] indexEntryQueryCache = mIndexEntryQueryCache;
if (indexEntryQueryCache == null) {
return null;
}
int key = 0;
if (rangeEndBoundary != BoundaryType.OPEN) {
key += (rangeEndBoundary == BoundaryType.INCLUSIVE) ? 1 : 2;
}
if (rangeStartBoundary != BoundaryType.OPEN) {
key += (rangeStartBoundary == BoundaryType.INCLUSIVE) ? (1 * 3) : (2 * 3);
}
Query<?> indexEntryQuery = indexEntryQueryCache[key];
if (indexEntryQuery == null) {
indexEntryQuery = mSupport.indexEntryQuery(mIndex);
Filter filter = indexEntryQuery.getFilter();
int i;
for (i=0; i<mIdentityCount; i++) {
filter = filter.and(mIndex.getProperty(i).getName(), RelOp.EQ);
}
if (rangeStartBoundary != BoundaryType.OPEN) {
RelOp op = (rangeStartBoundary == BoundaryType.INCLUSIVE) ? RelOp.GE : RelOp.GT;
filter = filter.and(mIndex.getProperty(i).getName(), op);
}
if (rangeEndBoundary != BoundaryType.OPEN) {
RelOp op = (rangeEndBoundary == BoundaryType.INCLUSIVE) ? RelOp.LE : RelOp.LT;
filter = filter.and(mIndex.getProperty(i).getName(), op);
}
if (mCoveringFilter != null) {
filter = filter.and(mCoveringFilter.unbind().toString());
}
indexEntryQuery = indexEntryQuery.and(filter);
// Enforce index ordering where applicable.
if (mIdentityCount < mIndex.getPropertyCount()) {
String[] orderProperties = new String[mIdentityCount + 1];
for (i=0; i<orderProperties.length; i++) {
Direction dir = mIndex.getPropertyDirection(i);
if (dir == Direction.UNSPECIFIED) {
dir = Direction.ASCENDING;
}
if (mReverseOrder) {
dir = dir.reverse();
}
orderProperties[i] = dir.toCharacter() + mIndex.getProperty(i).getName();
}
indexEntryQuery = indexEntryQuery.orderBy(orderProperties);
}
mIndexEntryQueryCache[key] = indexEntryQuery;
}
return indexEntryQuery;
}
/**
* Provides support for {@link IndexedQueryExecutor}.
*/
public static interface Support<S extends Storable> {
/**
* Returns an open query if the given index supports query access. If
* not supported, return null. An index entry query might be used to
* perform filtering and sorting of index entries prior to being
* resolved into referenced Storables.
*
* <p>If an index entry query is returned, the fetchSubset method is
* never called by IndexedQueryExecutor.
*
* @return index entry query or null if not supported
* @since 1.2
*/
Query<?> indexEntryQuery(StorableIndex<S> index) throws FetchException;
/**
* Fetch Storables referenced by the given index entry query. This
* method is only called if index supports query access.
*
* @param index index to open
* @param indexEntryQuery query with no blank parameters, derived from
* the query returned by indexEntryQuery
* @since 1.2
*/
Cursor<S> fetchFromIndexEntryQuery(StorableIndex<S> index, Query<?> indexEntryQuery)
throws FetchException;
/**
* Fetch Storables referenced by the given index entry query. This
* method is only called if index supports query access.
*
* @param index index to open
* @param indexEntryQuery query with no blank parameters, derived from
* the query returned by indexEntryQuery
* @param controller optional controller which can abort query operation
* @since 1.2
*/
Cursor<S> fetchFromIndexEntryQuery(StorableIndex<S> index, Query<?> indexEntryQuery,
Query.Controller controller)
throws FetchException;
/**
* Perform an index scan of a subset of Storables referenced by an
* index. The identity values are aligned with the index properties at
* property 0. An optional range start or range end aligns with the index
* property following the last of the identity values.
*
* <p>This method is only called if no index entry query was provided
* for the given index.
*
* @param index index to open, which may be a primary key index
* @param identityValues optional list of exactly matching values to apply to index
* @param rangeStartBoundary start boundary type
* @param rangeStartValue value to start at if boundary is not open
* @param rangeEndBoundary end boundary type
* @param rangeEndValue value to end at if boundary is not open
* @param reverseRange indicates that range operates on a property whose
* natural order is descending. Only the code that opens the physical
* cursor should examine this parameter. If true, then the range start and
* end parameter pairs need to be swapped.
* @param reverseOrder when true, iteration should be reversed from its
* natural order
*/
Cursor<S> fetchSubset(StorableIndex<S> index,
Object[] identityValues,
BoundaryType rangeStartBoundary,
Object rangeStartValue,
BoundaryType rangeEndBoundary,
Object rangeEndValue,
boolean reverseRange,
boolean reverseOrder)
throws FetchException;
/**
* Perform an index scan of a subset of Storables referenced by an
* index. The identity values are aligned with the index properties at
* property 0. An optional range start or range end aligns with the index
* property following the last of the identity values.
*
* <p>This method is only called if no index entry query was provided
* for the given index.
*
* @param index index to open, which may be a primary key index
* @param identityValues optional list of exactly matching values to apply to index
* @param rangeStartBoundary start boundary type
* @param rangeStartValue value to start at if boundary is not open
* @param rangeEndBoundary end boundary type
* @param rangeEndValue value to end at if boundary is not open
* @param reverseRange indicates that range operates on a property whose
* natural order is descending. Only the code that opens the physical
* cursor should examine this parameter. If true, then the range start and
* end parameter pairs need to be swapped.
* @param reverseOrder when true, iteration should be reversed from its
* natural order
* @param controller optional controller which can abort query operation
*/
Cursor<S> fetchSubset(StorableIndex<S> index,
Object[] identityValues,
BoundaryType rangeStartBoundary,
Object rangeStartValue,
BoundaryType rangeEndBoundary,
Object rangeEndValue,
boolean reverseRange,
boolean reverseOrder,
Query.Controller controller)
throws FetchException;
}
}