blob: c426d836bdd14413e99a137ffc9a792810728152 [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.ArrayList;
import java.util.List;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.io.WritableUtils;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.expression.visitor.ExpressionVisitor;
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.schema.types.PDecimal;
import org.apache.phoenix.util.ExpressionUtil;
/**
*
* CASE/WHEN expression implementation
*
*
* @since 0.1
*/
public class CaseExpression extends BaseCompoundExpression {
private static final int FULLY_EVALUATE = -1;
private short evalIndex = FULLY_EVALUATE;
private boolean foundIndex;
private PDataType returnType;
public CaseExpression() {
}
public static Expression create(List<Expression> children) throws SQLException {
CaseExpression caseExpression = new CaseExpression(coerceIfNecessary(children));
if (ExpressionUtil.isConstant(caseExpression)) {
ImmutableBytesWritable ptr = new ImmutableBytesWritable();
int index = caseExpression.evaluateIndexOf(null, ptr);
if (index < 0) {
return LiteralExpression.newConstant(null, caseExpression.getDeterminism());
}
return caseExpression.getChildren().get(index);
}
return caseExpression;
}
private static List<Expression> coerceIfNecessary(List<Expression> children) throws SQLException {
boolean isChildTypeUnknown = false;
PDataType returnType = children.get(0).getDataType();
for (int i = 2; i < children.size(); i+=2) {
Expression child = children.get(i);
PDataType childType = child.getDataType();
if (childType == null) {
isChildTypeUnknown = true;
} else if (returnType == null) {
returnType = childType;
isChildTypeUnknown = true;
} else if (returnType == childType || childType.isCoercibleTo(returnType)) {
continue;
} else if (returnType.isCoercibleTo(childType)) {
returnType = childType;
} else {
throw new SQLExceptionInfo.Builder(SQLExceptionCode.TYPE_MISMATCH)
.setMessage("Case expressions must have common type: " + returnType + " cannot be coerced to " + childType)
.build().buildException();
}
}
// If we found an "unknown" child type and the return type is a number
// make the return type be the most general number type of DECIMAL.
if (isChildTypeUnknown && returnType != null && returnType.isCoercibleTo(PDecimal.INSTANCE)) {
returnType = PDecimal.INSTANCE;
}
List<Expression> newChildren = children;
for (int i = 0; i < children.size(); i+=2) {
Expression child = children.get(i);
PDataType childType = child.getDataType();
if (childType != returnType) {
if (newChildren == children) {
newChildren = new ArrayList<Expression>(children);
}
newChildren.set(i, CoerceExpression.create(child, returnType));
}
}
return newChildren;
}
/**
* Construct CASE/WHEN expression
* @param expressions list of expressions in the form of:
* ((<result expression>, <boolean expression>)+, [<optional else result expression>])
* @throws SQLException if return type of case expressions do not match and cannot
* be coerced to a common type
*/
public CaseExpression(List<Expression> children) {
super(children);
returnType = children.get(0).getDataType();
}
private boolean isPartiallyEvaluating() {
return evalIndex != FULLY_EVALUATE;
}
public boolean hasElse() {
return children.size() % 2 != 0;
}
@Override
public boolean isNullable() {
// If any expression is nullable or there's no else clause
// return true since null may be returned.
if (super.isNullable() || !hasElse()) {
return true;
}
return children.get(children.size()-1).isNullable();
}
@Override
public PDataType getDataType() {
return returnType;
}
@Override
public void reset() {
foundIndex = false;
evalIndex = 0;
}
@Override
public void readFields(DataInput input) throws IOException {
super.readFields(input);
this.returnType = PDataType.values()[WritableUtils.readVInt(input)];
}
@Override
public void write(DataOutput output) throws IOException {
super.write(output);
WritableUtils.writeVInt(output, this.returnType.ordinal());
}
public int evaluateIndexOf(Tuple tuple, ImmutableBytesWritable ptr) {
if (foundIndex) {
return evalIndex;
}
int size = children.size();
// If we're doing partial evaluation, start where we left off
for (int i = isPartiallyEvaluating() ? evalIndex : 0; i < size; i+=2) {
// Short circuit if we see our stop value
if (i+1 == size) {
return i;
}
// If we get null, we have to re-evaluate from that point (special case this in filter, like is null)
// We may only run this when we're done/have all values
boolean evaluated = children.get(i+1).evaluate(tuple, ptr);
if (evaluated && Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(ptr))) {
if (isPartiallyEvaluating()) {
foundIndex = true;
}
return i;
}
if (isPartiallyEvaluating()) {
if (evaluated || tuple.isImmutable()) {
evalIndex+=2;
} else {
/*
* Return early here if incrementally evaluating and we don't
* have all the key values yet. We can't continue because we'd
* potentially be bypassing cases which we could later evaluate
* once we have more column values.
*/
return -1;
}
}
}
// No conditions matched, return size to indicate that we were able
// to evaluate all cases, but didn't find any matches.
return size;
}
/**
* Only expression that currently uses the isPartial flag. The IS NULL
* expression will use it too. TODO: We could alternatively have a non interface
* method, like setIsPartial in which we set to false prior to calling
* evaluate.
*/
@Override
public boolean evaluate(Tuple tuple, ImmutableBytesWritable ptr) {
int index = evaluateIndexOf(tuple, ptr);
if (index < 0) {
return false;
} else if (index == children.size()) {
ptr.set(PDataType.NULL_BYTES);
return true;
}
if (children.get(index).evaluate(tuple, ptr)) {
return true;
}
return false;
}
@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;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder("CASE ");
for (int i = 0; i < children.size() - 1; i+=2) {
buf.append("WHEN ");
buf.append(children.get(i+1));
buf.append(" THEN ");
buf.append(children.get(i));
}
if (hasElse()) {
buf.append(" ELSE " + children.get(children.size()-1));
}
buf.append(" END");
return buf.toString();
}
@Override
public boolean requiresFinalEvaluation() {
return super.requiresFinalEvaluation() || this.hasElse();
}
}