blob: a977f1fd5092115cfac000d7476aef466dffcf0c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.phoenix.expression;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.WritableUtils;
import org.apache.phoenix.expression.visitor.ExpressionVisitor;
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.tuple.Tuple;
import org.apache.phoenix.schema.types.PBoolean;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.ExpressionUtil;
import org.apache.phoenix.util.StringUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
/*
* Implementation of a SQL foo IN (a,b,c) expression. Other than the first
* expression, child expressions must be constants.
*
*/
public class InListExpression extends BaseSingleExpression {
private Set<ImmutableBytesPtr> values;
private ImmutableBytesPtr minValue;
private ImmutableBytesPtr maxValue;
private int valuesByteLength;
private int fixedWidth = -1;
private List<Expression> keyExpressions; // client side only
private boolean rowKeyOrderOptimizable; // client side only
public static Expression create (List<Expression> children, boolean isNegate, ImmutableBytesWritable ptr, boolean rowKeyOrderOptimizable) throws SQLException {
Expression firstChild = children.get(0);
if (firstChild.isStateless() && (!firstChild.evaluate(null, ptr) || ptr.getLength() == 0)) {
return LiteralExpression.newConstant(null, PBoolean.INSTANCE, firstChild.getDeterminism());
}
if (children.size() == 2) {
return ComparisonExpression.create(isNegate ? CompareOp.NOT_EQUAL : CompareOp.EQUAL, children, ptr, rowKeyOrderOptimizable);
}
boolean addedNull = false;
SQLException sqlE = null;
List<Expression> coercedKeyExpressions = Lists.newArrayListWithExpectedSize(children.size());
coercedKeyExpressions.add(firstChild);
for (int i = 1; i < children.size(); i++) {
try {
Expression rhs = BaseExpression.coerce(firstChild, children.get(i), CompareOp.EQUAL, rowKeyOrderOptimizable);
coercedKeyExpressions.add(rhs);
} catch (SQLException e) {
// Type mismatch exception or invalid data exception.
// Ignore and filter the element from the list and it means it cannot possibly
// be in the list. If list is empty, we'll throw the last exception we ignored,
// as this is an error condition.
sqlE = e;
}
}
if (coercedKeyExpressions.size() == 1) {
throw sqlE != null ? sqlE : new SQLException("Only one element in IN list");
}
if (coercedKeyExpressions.size() == 2 && addedNull) {
return LiteralExpression.newConstant(null, PBoolean.INSTANCE, Determinism.ALWAYS);
}
Expression expression = new InListExpression(coercedKeyExpressions, rowKeyOrderOptimizable);
if (isNegate) {
expression = NotExpression.create(expression, ptr);
}
if (ExpressionUtil.isConstant(expression)) {
return ExpressionUtil.getConstantExpression(expression, ptr);
}
return expression;
}
public InListExpression() {
}
public InListExpression(List<Expression> keyExpressions, boolean rowKeyOrderOptimizable) {
super(keyExpressions.get(0));
this.rowKeyOrderOptimizable = rowKeyOrderOptimizable;
Expression firstChild = keyExpressions.get(0);
this.keyExpressions = keyExpressions.subList(1, keyExpressions.size());
Set<ImmutableBytesPtr> values = Sets.newHashSetWithExpectedSize(keyExpressions.size()-1);
Integer maxLength = firstChild.getDataType().isFixedWidth() ? firstChild.getMaxLength() : null;
int fixedWidth = -1;
boolean isFixedLength = true;
for (int i = 1; i < keyExpressions.size(); i++) {
ImmutableBytesPtr ptr = new ImmutableBytesPtr();
Expression child = keyExpressions.get(i);
child.evaluate(null, ptr);
if (ptr.getLength() > 0) { // filter null as it has no impact
if (rowKeyOrderOptimizable) {
firstChild.getDataType().pad(ptr, maxLength, firstChild.getSortOrder());
} else if (maxLength != null) {
byte[] paddedBytes = StringUtil.padChar(ByteUtil.copyKeyBytesIfNecessary(ptr), maxLength);
ptr.set(paddedBytes);
}
if (values.add(ptr)) {
int length = ptr.getLength();
if (fixedWidth == -1) {
fixedWidth = length;
} else {
isFixedLength &= fixedWidth == length;
}
valuesByteLength += ptr.getLength();
}
}
}
this.fixedWidth = isFixedLength ? fixedWidth : -1;
// Sort values by byte value so we can get min/max easily
ImmutableBytesPtr[] valuesArray = values.toArray(new ImmutableBytesPtr[values.size()]);
Arrays.sort(valuesArray, ByteUtil.BYTES_PTR_COMPARATOR);
if (values.isEmpty()) {
this.minValue = ByteUtil.EMPTY_BYTE_ARRAY_PTR;
this.maxValue = ByteUtil.EMPTY_BYTE_ARRAY_PTR;
this.values = Collections.emptySet();
} else {
this.minValue = valuesArray[0];
this.maxValue = valuesArray[valuesArray.length-1];
// Use LinkedHashSet on client-side so that we don't need to serialize the
// minValue and maxValue but can infer them based on the first and last position.
this.values = new LinkedHashSet<ImmutableBytesPtr>(Arrays.asList(valuesArray));
}
}
@Override
public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
if (!getChild().evaluate(tuple, ptr)) {
return false;
}
if (ptr.getLength() == 0) { // null IN (...) is always null
ptr.set(ByteUtil.EMPTY_BYTE_ARRAY);
return true;
}
if (values.contains(ptr)) {
ptr.set(PDataType.TRUE_BYTES);
return true;
}
ptr.set(PDataType.FALSE_BYTES);
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + children.hashCode() + values.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
InListExpression other = (InListExpression)obj;
if (!children.equals(other.children) || !values.equals(other.values)) return false;
return true;
}
@Override
public PDataType getDataType() {
return PBoolean.INSTANCE;
}
private int readValue(DataInput input, byte[] valuesBytes, int offset, ImmutableBytesPtr ptr) throws IOException {
int valueLen = fixedWidth == -1 ? WritableUtils.readVInt(input) : fixedWidth;
values.add(new ImmutableBytesPtr(valuesBytes,offset,valueLen));
return offset + valueLen;
}
@Override
public void readFields(DataInput input) throws IOException {
super.readFields(input);
input.readBoolean(); // Unused, but left for b/w compat. TODO: remove in next major release
fixedWidth = WritableUtils.readVInt(input);
byte[] valuesBytes = Bytes.readByteArray(input);
valuesByteLength = valuesBytes.length;
int len = fixedWidth == -1 ? WritableUtils.readVInt(input) : valuesByteLength / fixedWidth;
// TODO: consider using a regular HashSet as we never serialize from the server-side
values = Sets.newLinkedHashSetWithExpectedSize(len);
int offset = 0;
int i = 0;
if (i < len) {
offset = readValue(input, valuesBytes, offset, minValue = new ImmutableBytesPtr());
while (++i < len-1) {
offset = readValue(input, valuesBytes, offset, new ImmutableBytesPtr());
}
if (i < len) {
offset = readValue(input, valuesBytes, offset, maxValue = new ImmutableBytesPtr());
} else {
maxValue = minValue;
}
} else {
minValue = maxValue = new ImmutableBytesPtr(ByteUtil.EMPTY_BYTE_ARRAY);
}
}
@Override
public void write(DataOutput output) throws IOException {
super.write(output);
output.writeBoolean(false); // Unused, but left for b/w compat. TODO: remove in next major release
WritableUtils.writeVInt(output, fixedWidth);
WritableUtils.writeVInt(output, valuesByteLength);
for (ImmutableBytesPtr ptr : values) {
output.write(ptr.get(), ptr.getOffset(), ptr.getLength());
}
if (fixedWidth == -1) {
WritableUtils.writeVInt(output, values.size());
for (ImmutableBytesPtr ptr : values) {
WritableUtils.writeVInt(output, ptr.getLength());
}
}
}
@Override
public final <T> T accept(ExpressionVisitor<T> visitor) {
List<T> l = acceptChildren(visitor, visitor.visitEnter(this));
T t = visitor.visitLeave(this, l);
if (t == null) {
t = visitor.defaultReturn(this, l);
}
return t;
}
public List<Expression> getKeyExpressions() {
return keyExpressions;
}
public ImmutableBytesWritable getMinKey() {
return minValue;
}
public ImmutableBytesWritable getMaxKey() {
return maxValue;
}
@Override
public String toString() {
int maxToStringLen = 200;
Expression firstChild = children.get(0);
PDataType type = firstChild.getDataType();
StringBuilder buf = new StringBuilder(firstChild + " IN (");
for (ImmutableBytesPtr value : values) {
if (firstChild.getSortOrder() != null) {
type.coerceBytes(value, type, firstChild.getSortOrder(), SortOrder.getDefault());
}
buf.append(type.toStringLiteral(value, null));
buf.append(',');
if (buf.length() >= maxToStringLen) {
buf.append("... ");
break;
}
}
buf.setCharAt(buf.length()-1,')');
return buf.toString();
}
public InListExpression clone(List<Expression> l) {
return new InListExpression(l, this.rowKeyOrderOptimizable);
}
}