blob: 8803583c4c8ce4ca6279c2d0aa74bd0fbc496b06 [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.druid.math.expr;
import com.google.common.primitives.Doubles;
import org.apache.druid.common.config.NullHandling;
import org.apache.druid.common.guava.GuavaUtils;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.java.util.common.UOE;
import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* Generic result holder for evaluated {@link Expr} containing the value and {@link ExprType} of the value to allow
*/
public abstract class ExprEval<T>
{
private static final int NULL_LENGTH = -1;
/**
* Deserialize an expression stored in a bytebuffer, e.g. for an agg.
*
* This should be refactored to be consolidated with some of the standard type handling of aggregators probably
*/
public static ExprEval deserialize(ByteBuffer buffer, int position)
{
final ExprType type = ExprType.fromByte(buffer.get(position));
return deserialize(buffer, position + 1, type);
}
/**
* Deserialize an expression stored in a bytebuffer, e.g. for an agg.
*
* This should be refactored to be consolidated with some of the standard type handling of aggregators probably
*/
public static ExprEval deserialize(ByteBuffer buffer, int offset, ExprType type)
{
// | expression bytes |
switch (type) {
case LONG:
// | expression type (byte) | is null (byte) | long bytes |
if (buffer.get(offset++) == NullHandling.IS_NOT_NULL_BYTE) {
return of(buffer.getLong(offset));
}
return ofLong(null);
case DOUBLE:
// | expression type (byte) | is null (byte) | double bytes |
if (buffer.get(offset++) == NullHandling.IS_NOT_NULL_BYTE) {
return of(buffer.getDouble(offset));
}
return ofDouble(null);
case STRING:
// | expression type (byte) | string length (int) | string bytes |
final int length = buffer.getInt(offset);
if (length < 0) {
return of(null);
}
final byte[] stringBytes = new byte[length];
final int oldPosition = buffer.position();
buffer.position(offset + Integer.BYTES);
buffer.get(stringBytes, 0, length);
buffer.position(oldPosition);
return of(StringUtils.fromUtf8(stringBytes));
case LONG_ARRAY:
// | expression type (byte) | array length (int) | array bytes |
final int longArrayLength = buffer.getInt(offset);
offset += Integer.BYTES;
if (longArrayLength < 0) {
return ofLongArray(null);
}
final Long[] longs = new Long[longArrayLength];
for (int i = 0; i < longArrayLength; i++) {
final byte isNull = buffer.get(offset);
offset += Byte.BYTES;
if (isNull == NullHandling.IS_NOT_NULL_BYTE) {
// | is null (byte) | long bytes |
longs[i] = buffer.getLong(offset);
offset += Long.BYTES;
} else {
// | is null (byte) |
longs[i] = null;
}
}
return ofLongArray(longs);
case DOUBLE_ARRAY:
// | expression type (byte) | array length (int) | array bytes |
final int doubleArrayLength = buffer.getInt(offset);
offset += Integer.BYTES;
if (doubleArrayLength < 0) {
return ofDoubleArray(null);
}
final Double[] doubles = new Double[doubleArrayLength];
for (int i = 0; i < doubleArrayLength; i++) {
final byte isNull = buffer.get(offset);
offset += Byte.BYTES;
if (isNull == NullHandling.IS_NOT_NULL_BYTE) {
// | is null (byte) | double bytes |
doubles[i] = buffer.getDouble(offset);
offset += Double.BYTES;
} else {
// | is null (byte) |
doubles[i] = null;
}
}
return ofDoubleArray(doubles);
case STRING_ARRAY:
// | expression type (byte) | array length (int) | array bytes |
final int stringArrayLength = buffer.getInt(offset);
offset += Integer.BYTES;
if (stringArrayLength < 0) {
return ofStringArray(null);
}
final String[] stringArray = new String[stringArrayLength];
for (int i = 0; i < stringArrayLength; i++) {
final int stringElementLength = buffer.getInt(offset);
offset += Integer.BYTES;
if (stringElementLength < 0) {
// | string length (int) |
stringArray[i] = null;
} else {
// | string length (int) | string bytes |
final byte[] stringElementBytes = new byte[stringElementLength];
final int oldPosition2 = buffer.position();
buffer.position(offset);
buffer.get(stringElementBytes, 0, stringElementLength);
buffer.position(oldPosition2);
stringArray[i] = StringUtils.fromUtf8(stringElementBytes);
offset += stringElementLength;
}
}
return ofStringArray(stringArray);
default:
throw new UOE("how can this be?");
}
}
/**
* Write an expression result to a bytebuffer, throwing an {@link ISE} if the data exceeds a maximum size. Primitive
* numeric types are not validated to be lower than max size, so it is expected to be at least 10 bytes. Callers
* of this method should enforce this themselves (instead of doing it here, which might be done every row)
*
* This should be refactored to be consolidated with some of the standard type handling of aggregators probably
*/
public static void serialize(ByteBuffer buffer, int position, ExprEval<?> eval, int maxSizeBytes)
{
int offset = position;
buffer.put(offset++, eval.type().getId());
switch (eval.type()) {
case LONG:
if (eval.isNumericNull()) {
buffer.put(offset, NullHandling.IS_NULL_BYTE);
} else {
buffer.put(offset++, NullHandling.IS_NOT_NULL_BYTE);
buffer.putLong(offset, eval.asLong());
}
break;
case DOUBLE:
if (eval.isNumericNull()) {
buffer.put(offset, NullHandling.IS_NULL_BYTE);
} else {
buffer.put(offset++, NullHandling.IS_NOT_NULL_BYTE);
buffer.putDouble(offset, eval.asDouble());
}
break;
case STRING:
final byte[] stringBytes = StringUtils.toUtf8Nullable(eval.asString());
if (stringBytes != null) {
// | expression type (byte) | string length (int) | string bytes |
checkMaxBytes(eval.type(), 1 + Integer.BYTES + stringBytes.length, maxSizeBytes);
buffer.putInt(offset, stringBytes.length);
offset += Integer.BYTES;
final int oldPosition = buffer.position();
buffer.position(offset);
buffer.put(stringBytes, 0, stringBytes.length);
buffer.position(oldPosition);
} else {
checkMaxBytes(eval.type(), 1 + Integer.BYTES, maxSizeBytes);
buffer.putInt(offset, NULL_LENGTH);
}
break;
case LONG_ARRAY:
Long[] longs = eval.asLongArray();
if (longs == null) {
// | expression type (byte) | array length (int) |
checkMaxBytes(eval.type(), 1 + Integer.BYTES, maxSizeBytes);
buffer.putInt(offset, NULL_LENGTH);
} else {
// | expression type (byte) | array length (int) | array bytes |
final int sizeBytes = 1 + Integer.BYTES + (Long.BYTES * longs.length);
checkMaxBytes(eval.type(), sizeBytes, maxSizeBytes);
buffer.putInt(offset, longs.length);
offset += Integer.BYTES;
for (Long aLong : longs) {
if (aLong != null) {
buffer.put(offset, NullHandling.IS_NOT_NULL_BYTE);
offset++;
buffer.putLong(offset, aLong);
offset += Long.BYTES;
} else {
buffer.put(offset++, NullHandling.IS_NULL_BYTE);
}
}
}
break;
case DOUBLE_ARRAY:
Double[] doubles = eval.asDoubleArray();
if (doubles == null) {
// | expression type (byte) | array length (int) |
checkMaxBytes(eval.type(), 1 + Integer.BYTES, maxSizeBytes);
buffer.putInt(offset, NULL_LENGTH);
} else {
// | expression type (byte) | array length (int) | array bytes |
final int sizeBytes = 1 + Integer.BYTES + (Double.BYTES * doubles.length);
checkMaxBytes(eval.type(), sizeBytes, maxSizeBytes);
buffer.putInt(offset, doubles.length);
offset += Integer.BYTES;
for (Double aDouble : doubles) {
if (aDouble != null) {
buffer.put(offset, NullHandling.IS_NOT_NULL_BYTE);
offset++;
buffer.putDouble(offset, aDouble);
offset += Long.BYTES;
} else {
buffer.put(offset++, NullHandling.IS_NULL_BYTE);
}
}
}
break;
case STRING_ARRAY:
String[] strings = eval.asStringArray();
if (strings == null) {
// | expression type (byte) | array length (int) |
checkMaxBytes(eval.type(), 1 + Integer.BYTES, maxSizeBytes);
buffer.putInt(offset, NULL_LENGTH);
} else {
// | expression type (byte) | array length (int) | array bytes |
buffer.putInt(offset, strings.length);
offset += Integer.BYTES;
int sizeBytes = 1 + Integer.BYTES;
for (String string : strings) {
if (string == null) {
// | string length (int) |
sizeBytes += Integer.BYTES;
checkMaxBytes(eval.type(), sizeBytes, maxSizeBytes);
buffer.putInt(offset, NULL_LENGTH);
offset += Integer.BYTES;
} else {
// | string length (int) | string bytes |
final byte[] stringElementBytes = StringUtils.toUtf8(string);
sizeBytes += Integer.BYTES + stringElementBytes.length;
checkMaxBytes(eval.type(), sizeBytes, maxSizeBytes);
buffer.putInt(offset, stringElementBytes.length);
offset += Integer.BYTES;
final int oldPosition = buffer.position();
buffer.position(offset);
buffer.put(stringElementBytes, 0, stringElementBytes.length);
buffer.position(oldPosition);
offset += stringElementBytes.length;
}
}
}
break;
default:
throw new UOE("how can this be?");
}
}
public static void checkMaxBytes(ExprType type, int sizeBytes, int maxSizeBytes)
{
if (sizeBytes > maxSizeBytes) {
throw new ISE("Unable to serialize [%s], size [%s] is larger than max [%s]", type, sizeBytes, maxSizeBytes);
}
}
/**
* Used to estimate the size in bytes to {@link #serialize} the {@link ExprEval} value, checking against a maximum
* size and failing with an {@link ISE} if the estimate is over the maximum.
*/
public static void estimateAndCheckMaxBytes(ExprEval eval, int maxSizeBytes)
{
final int estimated;
switch (eval.type()) {
case STRING:
String stringValue = eval.asString();
estimated = 1 + Integer.BYTES + (stringValue == null ? 0 : StringUtils.estimatedBinaryLengthAsUTF8(stringValue));
break;
case LONG:
case DOUBLE:
estimated = 1 + (NullHandling.sqlCompatible() ? 1 + Long.BYTES : Long.BYTES);
break;
case STRING_ARRAY:
String[] stringArray = eval.asStringArray();
if (stringArray == null) {
estimated = 1 + Integer.BYTES;
} else {
final int elementsSize = Arrays.stream(stringArray)
.filter(Objects::nonNull)
.mapToInt(StringUtils::estimatedBinaryLengthAsUTF8)
.sum();
// since each value is variably sized, there is an integer per element
estimated = 1 + Integer.BYTES + (Integer.BYTES * stringArray.length) + elementsSize;
}
break;
case LONG_ARRAY:
Long[] longArray = eval.asLongArray();
if (longArray == null) {
estimated = 1 + Integer.BYTES;
} else {
final int elementsSize = Arrays.stream(longArray)
.filter(Objects::nonNull)
.mapToInt(x -> Long.BYTES)
.sum();
estimated = 1 + Integer.BYTES + (NullHandling.sqlCompatible() ? longArray.length : 0) + elementsSize;
}
break;
case DOUBLE_ARRAY:
Double[] doubleArray = eval.asDoubleArray();
if (doubleArray == null) {
estimated = 1 + Integer.BYTES;
} else {
final int elementsSize = Arrays.stream(doubleArray)
.filter(Objects::nonNull)
.mapToInt(x -> Long.BYTES)
.sum();
estimated = 1 + Integer.BYTES + (NullHandling.sqlCompatible() ? doubleArray.length : 0) + elementsSize;
}
break;
default:
throw new IllegalStateException("impossible");
}
checkMaxBytes(eval.type(), estimated, maxSizeBytes);
}
/**
* Converts a List to an appropriate array type, optionally doing some conversion to make multi-valued strings
* consistent across selector types, which are not consistent in treatment of null, [], and [null].
*
* If homogenizeMultiValueStrings is true, null and [] will be converted to [null], otherwise they will retain
*/
@Nullable
public static Object coerceListToArray(@Nullable List<?> val, boolean homogenizeMultiValueStrings)
{
// if value is not null and has at least 1 element, conversion is unambigous regardless of the selector
if (val != null && val.size() > 0) {
Class<?> coercedType = null;
for (Object elem : val) {
if (elem != null) {
coercedType = convertType(coercedType, elem.getClass());
}
}
if (coercedType == Long.class || coercedType == Integer.class) {
return val.stream().map(x -> x != null ? ((Number) x).longValue() : null).toArray(Long[]::new);
}
if (coercedType == Float.class || coercedType == Double.class) {
return val.stream().map(x -> x != null ? ((Number) x).doubleValue() : null).toArray(Double[]::new);
}
// default to string
return val.stream().map(x -> x != null ? x.toString() : null).toArray(String[]::new);
}
if (homogenizeMultiValueStrings) {
return new String[]{null};
} else {
if (val != null) {
return new String[0];
}
return null;
}
}
/**
* Find the common type to use between 2 types, useful for choosing the appropriate type for an array given a set
* of objects with unknown type, following rules similar to Java, our own native Expr, and SQL implicit type
* conversions. This is used to assist in preparing native java objects for {@link Expr.ObjectBinding} which will
* later be wrapped in {@link ExprEval} when evaluating {@link IdentifierExpr}.
*
* If any type is string, then the result will be string because everything can be converted to a string, but a string
* cannot be converted to everything.
*
* For numbers, integer is the most restrictive type, only chosen if both types are integers. Longs win over integers,
* floats over longs and integers, and doubles win over everything.
*/
private static Class convertType(@Nullable Class existing, Class next)
{
if (Number.class.isAssignableFrom(next) || next == String.class) {
if (existing == null) {
return next;
}
// string wins everything
if (existing == String.class) {
return existing;
}
if (next == String.class) {
return next;
}
// all numbers win over Integer
if (existing == Integer.class) {
return next;
}
if (existing == Float.class) {
// doubles win over floats
if (next == Double.class) {
return next;
}
return existing;
}
if (existing == Long.class) {
if (next == Integer.class) {
// long beats int
return existing;
}
// double and float win over longs
return next;
}
// otherwise double
return Double.class;
}
throw new UOE("Invalid array expression type: %s", next);
}
public static ExprEval of(long longValue)
{
return new LongExprEval(longValue);
}
public static ExprEval of(double doubleValue)
{
return new DoubleExprEval(doubleValue);
}
public static ExprEval of(@Nullable String stringValue)
{
if (stringValue == null) {
return StringExprEval.OF_NULL;
}
return new StringExprEval(stringValue);
}
public static ExprEval ofLong(@Nullable Number longValue)
{
if (longValue == null) {
return LongExprEval.OF_NULL;
}
return new LongExprEval(longValue);
}
public static ExprEval ofDouble(@Nullable Number doubleValue)
{
if (doubleValue == null) {
return DoubleExprEval.OF_NULL;
}
return new DoubleExprEval(doubleValue);
}
public static ExprEval ofLongArray(@Nullable Long[] longValue)
{
if (longValue == null) {
return LongArrayExprEval.OF_NULL;
}
return new LongArrayExprEval(longValue);
}
public static ExprEval ofDoubleArray(@Nullable Double[] doubleValue)
{
if (doubleValue == null) {
return DoubleArrayExprEval.OF_NULL;
}
return new DoubleArrayExprEval(doubleValue);
}
public static ExprEval ofStringArray(@Nullable String[] stringValue)
{
if (stringValue == null) {
return StringArrayExprEval.OF_NULL;
}
return new StringArrayExprEval(stringValue);
}
/**
* Convert a boolean back into native expression type
*/
public static ExprEval ofBoolean(boolean value, ExprType type)
{
switch (type) {
case DOUBLE:
return ExprEval.of(Evals.asDouble(value));
case LONG:
return ExprEval.of(Evals.asLong(value));
case STRING:
return ExprEval.of(String.valueOf(value));
default:
throw new IllegalArgumentException("invalid type " + type);
}
}
/**
* Convert a boolean into a long expression type
*/
public static ExprEval ofLongBoolean(boolean value)
{
return ExprEval.of(Evals.asLong(value));
}
/**
* Examine java type to find most appropriate expression type
*/
public static ExprEval bestEffortOf(@Nullable Object val)
{
if (val instanceof ExprEval) {
return (ExprEval) val;
}
if (val instanceof Number) {
if (val instanceof Float || val instanceof Double) {
return new DoubleExprEval((Number) val);
}
return new LongExprEval((Number) val);
}
if (val instanceof Long[]) {
return new LongArrayExprEval((Long[]) val);
}
if (val instanceof Double[]) {
return new DoubleArrayExprEval((Double[]) val);
}
if (val instanceof Float[]) {
return new DoubleArrayExprEval(Arrays.stream((Float[]) val).map(Float::doubleValue).toArray(Double[]::new));
}
if (val instanceof String[]) {
return new StringArrayExprEval((String[]) val);
}
if (val instanceof List) {
// do not convert empty lists to arrays with a single null element here, because that should have been done
// by the selectors preparing their ObjectBindings if necessary. If we get to this point it was legitimately
// empty
return bestEffortOf(coerceListToArray((List<?>) val, false));
}
return new StringExprEval(val == null ? null : String.valueOf(val));
}
@Nullable
public static Number computeNumber(@Nullable String value)
{
if (value == null) {
return null;
}
Number rv;
Long v = GuavaUtils.tryParseLong(value);
// Do NOT use ternary operator here, because it makes Java to convert Long to Double
if (v != null) {
rv = v;
} else {
rv = Doubles.tryParse(value);
}
return rv;
}
// Cached String values
private boolean stringValueCached = false;
@Nullable
private String stringValue;
@Nullable
final T value;
private ExprEval(@Nullable T value)
{
this.value = value;
}
public abstract ExprType type();
@Nullable
public T value()
{
return value;
}
void cacheStringValue(@Nullable String value)
{
stringValue = value;
stringValueCached = true;
}
@Nullable
String getCachedStringValue()
{
assert stringValueCached;
return stringValue;
}
boolean isStringValueCached()
{
return stringValueCached;
}
@Nullable
public String asString()
{
if (!stringValueCached) {
if (value == null) {
stringValue = null;
} else {
stringValue = String.valueOf(value);
}
stringValueCached = true;
}
return stringValue;
}
/**
* The method returns true if numeric primitive value for this {@link ExprEval} is null, otherwise false.
*
* If this method returns false, then the values returned by {@link #asLong()}, {@link #asDouble()},
* and {@link #asInt()} are "valid", since this method is primarily used during {@link Expr} evaluation to decide
* if primitive numbers can be fetched to use.
*
* If a type cannot sanely convert into a primitive numeric value, then this method should always return true so that
* these primitive numeric getters are not called, since returning false is assumed to mean these values are valid.
*
* Note that all types must still return values for {@link #asInt()}, {@link #asLong()}}, and {@link #asDouble()},
* since this can still happen if {@link NullHandling#sqlCompatible()} is false, but it should be assumed that this
* can only happen in that mode and 0s are typical and expected for values that would otherwise be null.
*/
public abstract boolean isNumericNull();
public boolean isArray()
{
return false;
}
/**
* Get the primtive integer value. Callers should check {@link #isNumericNull()} prior to calling this method,
* otherwise it may improperly return placeholder a value (typically zero, which is expected if
* {@link NullHandling#sqlCompatible()} is false)
*/
public abstract int asInt();
/**
* Get the primtive long value. Callers should check {@link #isNumericNull()} prior to calling this method,
* otherwise it may improperly return a placeholder value (typically zero, which is expected if
* {@link NullHandling#sqlCompatible()} is false)
*/
public abstract long asLong();
/**
* Get the primtive double value. Callers should check {@link #isNumericNull()} prior to calling this method,
* otherwise it may improperly return a placeholder value (typically zero, which is expected if
* {@link NullHandling#sqlCompatible()} is false)
*/
public abstract double asDouble();
public abstract boolean asBoolean();
@Nullable
public abstract Object[] asArray();
@Nullable
public abstract String[] asStringArray();
@Nullable
public abstract Long[] asLongArray();
@Nullable
public abstract Double[] asDoubleArray();
public abstract ExprEval castTo(ExprType castTo);
public abstract Expr toExpr();
private abstract static class NumericExprEval extends ExprEval<Number>
{
private NumericExprEval(@Nullable Number value)
{
super(value);
}
@Override
public final int asInt()
{
return value.intValue();
}
@Override
public final long asLong()
{
return value.longValue();
}
@Override
public final double asDouble()
{
return value.doubleValue();
}
@Nullable
@Override
public String[] asStringArray()
{
return isNumericNull() ? null : new String[] {value.toString()};
}
@Nullable
@Override
public Long[] asLongArray()
{
return isNumericNull() ? null : new Long[] {value.longValue()};
}
@Nullable
@Override
public Double[] asDoubleArray()
{
return isNumericNull() ? null : new Double[] {value.doubleValue()};
}
@Override
public boolean isNumericNull()
{
return value == null;
}
}
private static class DoubleExprEval extends NumericExprEval
{
private static final DoubleExprEval OF_NULL = new DoubleExprEval(null);
private DoubleExprEval(@Nullable Number value)
{
super(value == null ? NullHandling.defaultDoubleValue() : (Double) value.doubleValue());
}
@Override
public final ExprType type()
{
return ExprType.DOUBLE;
}
@Override
public final boolean asBoolean()
{
return Evals.asBoolean(asDouble());
}
@Nullable
@Override
public Object[] asArray()
{
return asDoubleArray();
}
@Override
public final ExprEval castTo(ExprType castTo)
{
switch (castTo) {
case DOUBLE:
return this;
case LONG:
if (value == null) {
return ExprEval.ofLong(null);
} else {
return ExprEval.of(asLong());
}
case STRING:
return ExprEval.of(asString());
case DOUBLE_ARRAY:
return ExprEval.ofDoubleArray(asDoubleArray());
case LONG_ARRAY:
return ExprEval.ofLongArray(asLongArray());
case STRING_ARRAY:
return ExprEval.ofStringArray(asStringArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
if (isNumericNull()) {
return new NullDoubleExpr();
}
return new DoubleExpr(value.doubleValue());
}
}
private static class LongExprEval extends NumericExprEval
{
private static final LongExprEval OF_NULL = new LongExprEval(null);
private LongExprEval(@Nullable Number value)
{
super(value == null ? NullHandling.defaultLongValue() : (Long) value.longValue());
}
@Override
public final ExprType type()
{
return ExprType.LONG;
}
@Override
public final boolean asBoolean()
{
return Evals.asBoolean(asLong());
}
@Nullable
@Override
public Object[] asArray()
{
return asLongArray();
}
@Nullable
@Override
public Long[] asLongArray()
{
return isNumericNull() ? null : new Long[]{value.longValue()};
}
@Override
public final ExprEval castTo(ExprType castTo)
{
switch (castTo) {
case DOUBLE:
if (value == null) {
return ExprEval.ofDouble(null);
} else {
return ExprEval.of(asDouble());
}
case LONG:
return this;
case STRING:
return ExprEval.of(asString());
case DOUBLE_ARRAY:
return ExprEval.ofDoubleArray(asDoubleArray());
case LONG_ARRAY:
return ExprEval.ofLongArray(asLongArray());
case STRING_ARRAY:
return ExprEval.ofStringArray(asStringArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
if (isNumericNull()) {
return new NullLongExpr();
}
return new LongExpr(value.longValue());
}
}
private static class StringExprEval extends ExprEval<String>
{
private static final StringExprEval OF_NULL = new StringExprEval(null);
// Cached primitive values.
private boolean intValueValid = false;
private boolean longValueValid = false;
private boolean doubleValueValid = false;
private boolean booleanValueValid = false;
private int intValue;
private long longValue;
private double doubleValue;
private boolean booleanValue;
@Nullable
private Number numericVal;
private StringExprEval(@Nullable String value)
{
super(NullHandling.emptyToNullIfNeeded(value));
}
@Override
public final ExprType type()
{
return ExprType.STRING;
}
@Override
public int asInt()
{
if (!intValueValid) {
intValue = computeInt();
intValueValid = true;
}
return intValue;
}
@Override
public long asLong()
{
if (!longValueValid) {
longValue = computeLong();
longValueValid = true;
}
return longValue;
}
@Override
public double asDouble()
{
if (!doubleValueValid) {
doubleValue = computeDouble();
doubleValueValid = true;
}
return doubleValue;
}
@Nullable
@Override
public String asString()
{
return value;
}
@Nullable
@Override
public Object[] asArray()
{
return asStringArray();
}
private int computeInt()
{
Number number = computeNumber();
if (number == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return number.intValue();
}
private long computeLong()
{
Number number = computeNumber();
if (number == null) {
assert NullHandling.replaceWithDefault();
return 0L;
}
return number.longValue();
}
private double computeDouble()
{
Number number = computeNumber();
if (number == null) {
assert NullHandling.replaceWithDefault();
return 0.0d;
}
return number.doubleValue();
}
@Nullable
Number computeNumber()
{
if (value == null) {
return null;
}
if (numericVal != null) {
// Optimization for non-null case.
return numericVal;
}
numericVal = computeNumber(value);
return numericVal;
}
@Override
public boolean isNumericNull()
{
return computeNumber() == null;
}
@Override
public final boolean asBoolean()
{
if (!booleanValueValid) {
booleanValue = Evals.asBoolean(value);
booleanValueValid = true;
}
return booleanValue;
}
@Nullable
@Override
public String[] asStringArray()
{
return value == null ? null : new String[] {value};
}
@Nullable
@Override
public Long[] asLongArray()
{
return value == null ? null : new Long[] {computeLong()};
}
@Nullable
@Override
public Double[] asDoubleArray()
{
return value == null ? null : new Double[] {computeDouble()};
}
@Override
public final ExprEval castTo(ExprType castTo)
{
switch (castTo) {
case DOUBLE:
return ExprEval.ofDouble(computeNumber());
case LONG:
return ExprEval.ofLong(computeNumber());
case STRING:
return this;
case DOUBLE_ARRAY:
return ExprEval.ofDoubleArray(asDoubleArray());
case LONG_ARRAY:
return ExprEval.ofLongArray(asLongArray());
case STRING_ARRAY:
return ExprEval.ofStringArray(asStringArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
return new StringExpr(value);
}
}
abstract static class ArrayExprEval<T> extends ExprEval<T[]>
{
private ArrayExprEval(@Nullable T[] value)
{
super(value);
}
@Override
@Nullable
public String asString()
{
if (!isStringValueCached()) {
if (value == null) {
cacheStringValue(null);
} else if (value.length == 1) {
if (value[0] == null) {
cacheStringValue(null);
} else {
cacheStringValue(String.valueOf(value[0]));
}
} else {
cacheStringValue(Arrays.toString(value));
}
}
return getCachedStringValue();
}
@Override
public boolean isNumericNull()
{
if (isScalar()) {
return getScalarValue() == null;
}
return true;
}
@Override
public boolean isArray()
{
return true;
}
@Override
public int asInt()
{
return 0;
}
@Override
public long asLong()
{
return 0;
}
@Override
public double asDouble()
{
return 0;
}
@Override
public boolean asBoolean()
{
return false;
}
@Nullable
@Override
public T[] asArray()
{
return value;
}
@Nullable
public T getIndex(int index)
{
return value == null ? null : value[index];
}
protected boolean isScalar()
{
return value != null && value.length == 1;
}
@Nullable
protected T getScalarValue()
{
assert value != null && value.length == 1;
return value[0];
}
}
private static class LongArrayExprEval extends ArrayExprEval<Long>
{
private static final LongArrayExprEval OF_NULL = new LongArrayExprEval(null);
private LongArrayExprEval(@Nullable Long[] value)
{
super(value);
}
@Override
public ExprType type()
{
return ExprType.LONG_ARRAY;
}
@Override
public int asInt()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.intValue();
}
return super.asInt();
}
@Override
public long asLong()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.longValue();
}
return super.asLong();
}
@Override
public double asDouble()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.doubleValue();
}
return super.asDouble();
}
@Override
public boolean asBoolean()
{
if (isScalar()) {
Number scalarValue = getScalarValue();
if (scalarValue == null) {
assert NullHandling.replaceWithDefault();
return false;
}
return Evals.asBoolean(scalarValue.longValue());
}
return super.asBoolean();
}
@Nullable
@Override
public String[] asStringArray()
{
return value == null ? null : Arrays.stream(value).map(x -> x != null ? x.toString() : null).toArray(String[]::new);
}
@Nullable
@Override
public Long[] asLongArray()
{
return value;
}
@Nullable
@Override
public Double[] asDoubleArray()
{
return value == null ? null : Arrays.stream(value).map(Long::doubleValue).toArray(Double[]::new);
}
@Override
public ExprEval castTo(ExprType castTo)
{
if (value == null) {
return StringExprEval.OF_NULL;
}
switch (castTo) {
case STRING:
if (value.length == 1) {
return ExprEval.of(asString());
}
break;
case LONG:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofLong(null) : ExprEval.ofLong(asLong());
}
break;
case DOUBLE:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofDouble(null) : ExprEval.ofDouble(asDouble());
}
break;
case LONG_ARRAY:
return this;
case DOUBLE_ARRAY:
return ExprEval.ofDoubleArray(asDoubleArray());
case STRING_ARRAY:
return ExprEval.ofStringArray(asStringArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
return new LongArrayExpr(value);
}
}
private static class DoubleArrayExprEval extends ArrayExprEval<Double>
{
private static final DoubleArrayExprEval OF_NULL = new DoubleArrayExprEval(null);
private DoubleArrayExprEval(@Nullable Double[] value)
{
super(value);
}
@Override
public ExprType type()
{
return ExprType.DOUBLE_ARRAY;
}
@Override
public int asInt()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.intValue();
}
return super.asInt();
}
@Override
public long asLong()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.longValue();
}
return super.asLong();
}
@Override
public double asDouble()
{
if (isScalar()) {
Number scalar = getScalarValue();
if (scalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return scalar.doubleValue();
}
return super.asDouble();
}
@Override
public boolean asBoolean()
{
if (isScalar()) {
Number scalarValue = getScalarValue();
if (scalarValue == null) {
assert NullHandling.replaceWithDefault();
return false;
}
return Evals.asBoolean(scalarValue.longValue());
}
return super.asBoolean();
}
@Nullable
@Override
public String[] asStringArray()
{
return value == null
? null
: Arrays.stream(value).map(x -> x != null ? x.toString() : null).toArray(String[]::new);
}
@Nullable
@Override
public Long[] asLongArray()
{
return value == null ? null : Arrays.stream(value).map(Double::longValue).toArray(Long[]::new);
}
@Nullable
@Override
public Double[] asDoubleArray()
{
return value;
}
@Override
public ExprEval castTo(ExprType castTo)
{
if (value == null) {
return StringExprEval.OF_NULL;
}
switch (castTo) {
case STRING:
if (value.length == 1) {
return ExprEval.of(asString());
}
break;
case LONG:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofLong(null) : ExprEval.ofLong(asLong());
}
break;
case DOUBLE:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofDouble(null) : ExprEval.ofDouble(asDouble());
}
break;
case LONG_ARRAY:
return ExprEval.ofLongArray(asLongArray());
case DOUBLE_ARRAY:
return this;
case STRING_ARRAY:
return ExprEval.ofStringArray(asStringArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
return new DoubleArrayExpr(value);
}
}
private static class StringArrayExprEval extends ArrayExprEval<String>
{
private static final StringArrayExprEval OF_NULL = new StringArrayExprEval(null);
private boolean longValueValid = false;
private boolean doubleValueValid = false;
@Nullable
private Long[] longValues;
@Nullable
private Double[] doubleValues;
@Nullable
private Number computedNumericScalar;
private boolean isScalarNumberValid;
private StringArrayExprEval(@Nullable String[] value)
{
super(value);
}
@Override
public ExprType type()
{
return ExprType.STRING_ARRAY;
}
@Override
public boolean isNumericNull()
{
if (isScalar()) {
computeScalarNumericIfNeeded();
return computedNumericScalar == null;
}
return true;
}
@Override
public int asInt()
{
if (isScalar()) {
computeScalarNumericIfNeeded();
if (computedNumericScalar == null) {
assert NullHandling.replaceWithDefault();
return 0;
}
return computedNumericScalar.intValue();
}
return super.asInt();
}
@Override
public long asLong()
{
if (isScalar()) {
computeScalarNumericIfNeeded();
if (computedNumericScalar == null) {
assert NullHandling.replaceWithDefault();
return 0L;
}
return computedNumericScalar.longValue();
}
return super.asLong();
}
@Override
public double asDouble()
{
if (isScalar()) {
computeScalarNumericIfNeeded();
if (computedNumericScalar == null) {
assert NullHandling.replaceWithDefault();
return 0.0;
}
return computedNumericScalar.doubleValue();
}
return super.asDouble();
}
@Override
public boolean asBoolean()
{
if (isScalar()) {
return Evals.asBoolean(getScalarValue());
}
return super.asBoolean();
}
@Nullable
@Override
public String[] asStringArray()
{
return value;
}
@Nullable
@Override
public Long[] asLongArray()
{
if (!longValueValid) {
longValues = computeLongs();
longValueValid = true;
}
return longValues;
}
@Nullable
@Override
public Double[] asDoubleArray()
{
if (!doubleValueValid) {
doubleValues = computeDoubles();
doubleValueValid = true;
}
return doubleValues;
}
@Override
public ExprEval castTo(ExprType castTo)
{
if (value == null) {
return StringExprEval.OF_NULL;
}
switch (castTo) {
case STRING:
if (value.length == 1) {
return ExprEval.of(asString());
}
break;
case LONG:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofLong(null) : ExprEval.ofLong(asLong());
}
break;
case DOUBLE:
if (value.length == 1) {
return isNumericNull() ? ExprEval.ofDouble(null) : ExprEval.ofDouble(asDouble());
}
break;
case STRING_ARRAY:
return this;
case LONG_ARRAY:
return ExprEval.ofLongArray(asLongArray());
case DOUBLE_ARRAY:
return ExprEval.ofDoubleArray(asDoubleArray());
}
throw new IAE("invalid type " + castTo);
}
@Override
public Expr toExpr()
{
return new StringArrayExpr(value);
}
@Nullable
private Long[] computeLongs()
{
if (value == null) {
return null;
}
return Arrays.stream(value).map(value -> {
if (value == null) {
return null;
}
Long lv = GuavaUtils.tryParseLong(value);
if (lv == null) {
Double d = Doubles.tryParse(value);
if (d != null) {
lv = d.longValue();
}
}
return lv;
}).toArray(Long[]::new);
}
@Nullable
private Double[] computeDoubles()
{
if (value == null) {
return null;
}
return Arrays.stream(value).map(val -> {
if (val == null) {
return null;
}
return Doubles.tryParse(val);
}).toArray(Double[]::new);
}
/**
* must not be called unless array has a single element
*/
private void computeScalarNumericIfNeeded()
{
if (!isScalarNumberValid) {
computedNumericScalar = computeNumber(getScalarValue());
isScalarNumberValid = true;
}
}
}
}