/*******************************************************************************
* Copyright (c) 2013, Salesforce.com, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* Neither the name of Salesforce.com nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package com.salesforce.phoenix.util;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.TreeMap;
import org.apache.hadoop.hbase.client.Mutation;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.filter.Filter;
import org.apache.hadoop.hbase.filter.FilterList;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import com.google.common.collect.Lists;
import com.salesforce.phoenix.compile.ScanRanges;
import com.salesforce.phoenix.coprocessor.MetaDataProtocol;
import com.salesforce.phoenix.filter.SkipScanFilter;
import com.salesforce.phoenix.query.KeyRange;
import com.salesforce.phoenix.query.KeyRange.Bound;
import com.salesforce.phoenix.query.QueryConstants;
import com.salesforce.phoenix.schema.PDataType;
import com.salesforce.phoenix.schema.PTable;
import com.salesforce.phoenix.schema.RowKeySchema;
/**
*
* Various utilities for scans
*
* @author jtaylor
* @since 0.1
*/
public class ScanUtil {
private ScanUtil() {
}
public static void setTenantId(Scan scan, byte[] tenantId) {
scan.setAttribute(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
}
// Use getTenantId and pass in column name to match against
// in as PSchema attribute. If column name matches in
// KeyExpressions, set on scan as attribute
public static ImmutableBytesWritable getTenantId(Scan scan) {
// Create Scan with special aggregation column over which to aggregate
byte[] tenantId = scan.getAttribute(PhoenixRuntime.TENANT_ID_ATTRIB);
if (tenantId == null) {
return null;
}
return new ImmutableBytesWritable(tenantId);
}
public static Scan newScan(Scan scan) {
try {
Scan newScan = new Scan(scan);
// Clone the underlying family map instead of sharing it between
// the existing and cloned Scan (which is the retarded default
// behavior).
TreeMap<byte [], NavigableSet<byte []>> existingMap = (TreeMap<byte[], NavigableSet<byte[]>>)scan.getFamilyMap();
Map<byte [], NavigableSet<byte []>> clonedMap = new TreeMap<byte [], NavigableSet<byte []>>(existingMap);
newScan.setFamilyMap(clonedMap);
return newScan;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Intersects the scan start/stop row with the startKey and stopKey
* @param scan
* @param startKey
* @param stopKey
* @return false if the Scan cannot possibly return rows and true otherwise
*/
public static boolean intersectScanRange(Scan scan, byte[] startKey, byte[] stopKey) {
return intersectScanRange(scan, startKey, stopKey, false);
}
public static boolean intersectScanRange(Scan scan, byte[] startKey, byte[] stopKey, boolean useSkipScan) {
boolean mayHaveRows = false;
byte[] existingStartKey = scan.getStartRow();
byte[] existingStopKey = scan.getStopRow();
if (existingStartKey.length > 0) {
if (startKey.length == 0 || Bytes.compareTo(existingStartKey, startKey) > 0) {
startKey = existingStartKey;
}
} else {
mayHaveRows = true;
}
if (existingStopKey.length > 0) {
if (stopKey.length == 0 || Bytes.compareTo(existingStopKey, stopKey) < 0) {
stopKey = existingStopKey;
}
} else {
mayHaveRows = true;
}
scan.setStartRow(startKey);
scan.setStopRow(stopKey);
mayHaveRows = mayHaveRows || Bytes.compareTo(scan.getStartRow(), scan.getStopRow()) < 0;
// If the scan is using skip scan filter, intersect and replace the filter.
if (mayHaveRows && useSkipScan) {
Filter filter = scan.getFilter();
if (filter instanceof SkipScanFilter) {
SkipScanFilter oldFilter = (SkipScanFilter)filter;
SkipScanFilter newFilter = oldFilter.intersect(startKey, stopKey);
if (newFilter == null) {
return false;
}
// Intersect found: replace skip scan with intersected one
scan.setFilter(newFilter);
} else if (filter instanceof FilterList) {
FilterList filterList = (FilterList)filter;
Filter firstFilter = filterList.getFilters().get(0);
if (firstFilter instanceof SkipScanFilter) {
SkipScanFilter oldFilter = (SkipScanFilter)firstFilter;
SkipScanFilter newFilter = oldFilter.intersect(startKey, stopKey);
if (newFilter == null) {
return false;
}
// Intersect found: replace skip scan with intersected one
List<Filter> allFilters = new ArrayList<Filter>(filterList.getFilters().size());
allFilters.addAll(filterList.getFilters());
allFilters.set(0, newFilter);
scan.setFilter(new FilterList(FilterList.Operator.MUST_PASS_ALL,allFilters));
}
}
}
return mayHaveRows;
}
public static void andFilterAtBeginning(Scan scan, Filter andWithFilter) {
if (andWithFilter == null) {
return;
}
Filter filter = scan.getFilter();
if (filter == null) {
scan.setFilter(andWithFilter);
} else if (filter instanceof FilterList && ((FilterList)filter).getOperator() == FilterList.Operator.MUST_PASS_ALL) {
FilterList filterList = (FilterList)filter;
List<Filter> allFilters = new ArrayList<Filter>(filterList.getFilters().size() + 1);
allFilters.add(andWithFilter);
allFilters.addAll(filterList.getFilters());
scan.setFilter(new FilterList(FilterList.Operator.MUST_PASS_ALL,allFilters));
} else {
scan.setFilter(new FilterList(FilterList.Operator.MUST_PASS_ALL,Arrays.asList(andWithFilter, filter)));
}
}
public static void andFilterAtEnd(Scan scan, Filter andWithFilter) {
if (andWithFilter == null) {
return;
}
Filter filter = scan.getFilter();
if (filter == null) {
scan.setFilter(andWithFilter);
} else if (filter instanceof FilterList && ((FilterList)filter).getOperator() == FilterList.Operator.MUST_PASS_ALL) {
FilterList filterList = (FilterList)filter;
List<Filter> allFilters = new ArrayList<Filter>(filterList.getFilters().size() + 1);
allFilters.addAll(filterList.getFilters());
allFilters.add(andWithFilter);
scan.setFilter(new FilterList(FilterList.Operator.MUST_PASS_ALL,allFilters));
} else {
scan.setFilter(new FilterList(FilterList.Operator.MUST_PASS_ALL,Arrays.asList(filter, andWithFilter)));
}
}
public static void setTimeRange(Scan scan, long ts) {
try {
scan.setTimeRange(MetaDataProtocol.MIN_TABLE_TIMESTAMP, ts);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static byte[] getMinKey(RowKeySchema schema, List<List<KeyRange>> slots) {
return getKey(schema, slots, Bound.LOWER);
}
public static byte[] getMaxKey(RowKeySchema schema, List<List<KeyRange>> slots) {
return getKey(schema, slots, Bound.UPPER);
}
private static byte[] getKey(RowKeySchema schema, List<List<KeyRange>> slots, Bound bound) {
if (slots.isEmpty()) {
return null;
}
int[] position = new int[slots.size()];
int maxLength = 0;
for (int i = 0; i < position.length; i++) {
position[i] = bound == Bound.LOWER ? 0 : slots.get(i).size()-1;
KeyRange range = slots.get(i).get(position[i]);
maxLength += range.getRange(bound).length + (schema.getField(i).getDataType().isFixedWidth() ? 0 : 1);
}
byte[] key = new byte[maxLength];
int length = setKey(schema, slots, position, bound, key, 0, 0, position.length);
if (length == 0) {
return null;
}
if (length == maxLength) {
return key;
}
byte[] keyCopy = new byte[length];
System.arraycopy(key, 0, keyCopy, 0, length);
return keyCopy;
}
public static int estimateMaximumKeyLength(RowKeySchema schema, int schemaStartIndex, List<List<KeyRange>> slots) {
int maxLowerKeyLength = 0, maxUpperKeyLength = 0;
for (int i = 0; i < slots.size(); i++) {
int maxLowerRangeLength = 0, maxUpperRangeLength = 0;
for (KeyRange range: slots.get(i)) {
maxLowerRangeLength = Math.max(maxLowerRangeLength, range.getLowerRange().length);
maxUpperRangeLength = Math.max(maxUpperRangeLength, range.getUpperRange().length);
}
int trailingByte = (schema.getField(schemaStartIndex).getDataType().isFixedWidth() ||
schemaStartIndex == schema.getFieldCount() - 1 ? 0 : 1);
maxLowerKeyLength += maxLowerRangeLength + trailingByte;
maxUpperKeyLength += maxUpperKeyLength + trailingByte;
schemaStartIndex++;
}
return Math.max(maxLowerKeyLength, maxUpperKeyLength);
}
/*
* Set the key by appending the keyRanges inside slots at positions as specified by the position array.
*
* We need to increment part of the key range, or increment the whole key at the end, depending on the
* bound we are setting and whether the key range is inclusive or exclusive. The logic for determining
* whether to increment or not is:
* range/single boundary bound increment
* range inclusive lower no
* range inclusive upper yes, at the end if occurs at any slots.
* range exclusive lower yes
* range exclusive upper no
* single inclusive lower no
* single inclusive upper yes, at the end if it is the last slots.
*/
public static int setKey(RowKeySchema schema, List<List<KeyRange>> slots, int[] position, Bound bound,
byte[] key, int byteOffset, int slotStartIndex, int slotEndIndex) {
return setKey(schema, slots, position, bound, key, byteOffset, slotStartIndex, slotEndIndex, slotStartIndex);
}
public static int setKey(RowKeySchema schema, List<List<KeyRange>> slots, int[] position, Bound bound,
byte[] key, int byteOffset, int slotStartIndex, int slotEndIndex, int schemaStartIndex) {
int offset = byteOffset;
boolean lastInclusiveUpperSingleKey = false;
boolean anyInclusiveUpperRangeKey = false;
for (int i = slotStartIndex; i < slotEndIndex; i++) {
// Build up the key by appending the bound of each key range
// from the current position of each slot.
KeyRange range = slots.get(i).get(position[i]);
boolean isFixedWidth = schema.getField(schemaStartIndex++).getDataType().isFixedWidth();
/*
* If the current slot is unbound then stop if:
* 1) setting the upper bound. There's no value in
* continuing because nothing will be filtered.
* 2) setting the lower bound when the type is fixed length
* for the same reason. However, if the type is variable width
* continue building the key because null values will be filtered
* since our separator byte will be appended and incremented.
*/
if ( range.isUnbound(bound) &&
( bound == Bound.UPPER || isFixedWidth) ){
break;
}
byte[] bytes = range.getRange(bound);
System.arraycopy(bytes, 0, key, offset, bytes.length);
offset += bytes.length;
/*
* We must add a terminator to a variable length key even for the last PK column if
* the lower key is non inclusive or the upper key is inclusive. Otherwise, we'd be
* incrementing the key value itself, and thus bumping it up too much.
*/
boolean inclusiveUpper = range.isInclusive(bound) && bound == Bound.UPPER;
boolean exclusiveLower = !range.isInclusive(bound) && bound == Bound.LOWER;
if (!isFixedWidth && ( i < schema.getMaxFields()-1 || inclusiveUpper || exclusiveLower)) {
key[offset++] = QueryConstants.SEPARATOR_BYTE;
}
// If we are setting the upper bound of using inclusive single key, we remember
// to increment the key if we exit the loop after this iteration.
//
// We remember to increment the last slot if we are setting the upper bound with an
// inclusive range key.
//
// We cannot combine the two flags together in case for single-inclusive key followed
// by the range-exclusive key. In that case, we do not need to increment the end at the
// end. But if we combine the two flag, the single inclusive key in the middle of the
// key slots would cause the flag to become true.
lastInclusiveUpperSingleKey = range.isSingleKey() && inclusiveUpper;
anyInclusiveUpperRangeKey |= !range.isSingleKey() && inclusiveUpper;
// If we are setting the lower bound with an exclusive range key, we need to bump the
// slot up for each key part. For an upper bound, we bump up an inclusive key, but
// only after the last key part.
if (!range.isSingleKey() && exclusiveLower) {
if (!ByteUtil.nextKey(key, offset)) {
// Special case for not being able to increment.
// In this case we return a negative byteOffset to
// remove this part from the key being formed. Since the
// key has overflowed, this means that we should not
// have an end key specified.
return -byteOffset;
}
}
}
if (lastInclusiveUpperSingleKey || anyInclusiveUpperRangeKey) {
if (!ByteUtil.nextKey(key, offset)) {
// Special case for not being able to increment.
// In this case we return a negative byteOffset to
// remove this part from the key being formed. Since the
// key has overflowed, this means that we should not
// have an end key specified.
return -byteOffset;
}
}
// Remove trailing separator bytes, since the columns may have been added
// after the table has data, in which case there won't be a separator
// byte.
if (bound == Bound.LOWER) {
while (schemaStartIndex > 0 && offset > byteOffset &&
!schema.getField(--schemaStartIndex).getDataType().isFixedWidth() &&
key[offset-1] == QueryConstants.SEPARATOR_BYTE) {
offset--;
}
}
return offset - byteOffset;
}
public static boolean isAllSingleRowScan(List<List<KeyRange>> ranges, RowKeySchema schema) {
if (ranges.size() < schema.getMaxFields()) {
return false;
}
for (int i = 0; i < ranges.size(); i++) {
List<KeyRange> orRanges = ranges.get(i);
for (KeyRange range: orRanges) {
if (!range.isSingleKey()) {
return false;
}
}
}
return true;
}
/**
* Perform a binary lookup on the list of KeyRange for the tightest slot such that the slotBound
* of the current slot is higher or equal than the slotBound of our range.
* @return the index of the slot whose slot bound equals or are the tightest one that is
* smaller than rangeBound of range, or slots.length if no bound can be found.
*/
public static int searchClosestKeyRangeWithUpperHigherThanPtr(List<KeyRange> slots, ImmutableBytesWritable ptr, int lower) {
int upper = slots.size() - 1;
int mid;
while (lower <= upper) {
mid = (lower + upper) / 2;
int cmp = slots.get(mid).compareUpperToLowerBound(ptr, true);
if (cmp < 0) {
lower = mid + 1;
} else if (cmp > 0) {
upper = mid - 1;
} else {
return mid;
}
}
mid = (lower + upper) / 2;
if (mid == 0 && slots.get(mid).compareUpperToLowerBound(ptr, true) > 0) {
return mid;
} else {
return ++mid;
}
}
public static ScanRanges newScanRanges(List<Mutation> mutations) throws SQLException {
List<KeyRange> keys = Lists.newArrayListWithExpectedSize(mutations.size());
for (Mutation m : mutations) {
keys.add(PDataType.VARBINARY.getKeyRange(m.getRow()));
}
ScanRanges keyRanges = ScanRanges.create(Collections.singletonList(keys), SchemaUtil.VAR_BINARY_SCHEMA);
return keyRanges;
}
public static byte[] nextKey(byte[] key, PTable table, ImmutableBytesWritable ptr) {
int pos = 0;
RowKeySchema schema = table.getRowKeySchema();
int maxOffset = schema.iterator(key, ptr);
while (schema.next(ptr, pos, maxOffset) != null) {
pos++;
}
if (!schema.getField(pos-1).getDataType().isFixedWidth()) {
byte[] newLowerRange = new byte[key.length + 1];
System.arraycopy(key, 0, newLowerRange, 0, key.length);
newLowerRange[key.length] = QueryConstants.SEPARATOR_BYTE;
key = newLowerRange;
} else {
key = Arrays.copyOf(key, key.length);
}
ByteUtil.nextKey(key, key.length);
return key;
}
private static final String REVERSED_ATTR = "_reversed_";
public static void setReversed(Scan scan) {
// TODO: set attribute dynamically here to prevent dependency on newer HBase release
scan.setAttribute(REVERSED_ATTR, PDataType.TRUE_BYTES);
}
// Start/stop row must be swapped if scan is being done in reverse
public static void swapStartStopRowIfReversed(Scan scan) {
if (isReversed(scan)) {
byte[] startRow = scan.getStartRow();
byte[] stopRow = scan.getStopRow();
scan.setStartRow(stopRow);
scan.setStopRow(startRow);
}
}
public static boolean isReversed(Scan scan) {
byte[] reversed = scan.getAttribute(REVERSED_ATTR);
return (PDataType.TRUE_BYTES.equals(reversed));
}
}