/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.expressions;
import org.json_voltpatches.JSONException;
import org.json_voltpatches.JSONObject;
import org.json_voltpatches.JSONStringer;
import org.voltdb.VoltType;
import org.voltdb.parser.SQLParser;
import org.voltdb.planner.PlanningErrorException;
import org.voltdb.types.ExpressionType;
import org.voltdb.types.TimestampType;
import org.voltdb.utils.Encoder;
import org.voltdb.utils.VoltTypeUtil;
/**
*
*/
public class ConstantValueExpression extends AbstractValueExpression {
public enum Members {
VALUE,
ISNULL;
}
protected String m_value = null;
protected boolean m_isNull = true;
public ConstantValueExpression() {
super(ExpressionType.VALUE_CONSTANT);
}
@Override
public void validate() throws Exception {
super.validate();
// Make sure our value is not null
if (m_value == null && !m_isNull)
{
throw new Exception("ERROR: The constant value for '" + this +
"' is inconsistently null");
// Make sure the value type is something we support here
} else if (m_valueType == VoltType.NULL ||
m_valueType == VoltType.VOLTTABLE) {
throw new Exception("ERROR: Invalid constant value type '" + m_valueType + "' for '" + this + "'");
}
}
/**
* @return the value
*/
public String getValue() {
return m_value;
}
/**
* @param value the value to set
*/
public void setValue(String value) {
m_value = value;
m_isNull = false;
if (m_value == null)
{
m_isNull = true;
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ConstantValueExpression == false) {
return false;
}
ConstantValueExpression expr = (ConstantValueExpression) obj;
if (m_isNull != expr.m_isNull) {
return false;
}
if (m_isNull) { // implying that both sides are null
return true;
}
return m_value.equals(expr.m_value);
}
@Override
public int hashCode() {
// based on implementation of equals
int result = super.hashCode();
if (m_isNull) {
result += 1;
} else {
result += m_value.hashCode();
}
return result;
}
@Override
public void toJSONString(JSONStringer stringer) throws JSONException {
super.toJSONString(stringer);
stringer.keySymbolValuePair(Members.ISNULL.name(), m_isNull);
stringer.key(Members.VALUE.name());
if (m_isNull) {
stringer.value("NULL");
return;
}
{
switch (m_valueType)
{
case INVALID:
throw new JSONException("ConstantValueExpression.toJSONString(): value_type should never be VoltType.INVALID");
case NULL:
stringer.value("null");
break;
case TINYINT:
stringer.value(Long.valueOf(m_value));
break;
case SMALLINT:
stringer.value(Long.valueOf(m_value));
break;
case INTEGER:
stringer.value(Long.valueOf(m_value));
break;
case BIGINT:
stringer.value(Long.valueOf(m_value));
break;
case FLOAT:
stringer.value(Double.valueOf(m_value));
break;
case STRING:
stringer.value(m_value);
break;
case VARBINARY:
stringer.value(m_value);
break;
case TIMESTAMP:
stringer.value(Long.valueOf(m_value));
break;
case DECIMAL:
stringer.value(m_value);
break;
case BOOLEAN:
stringer.value(Boolean.valueOf(m_value));
break;
default:
throw new JSONException("ConstantValueExpression.toJSONString(): Unrecognized value_type " + m_valueType);
}
}
}
@Override
public void loadFromJSONObject(JSONObject obj) throws JSONException
{
m_isNull = false;
if (!obj.isNull(Members.VALUE.name())) {
m_value = obj.getString(Members.VALUE.name());
}
else {
m_isNull = true;
}
if (!obj.isNull(Members.ISNULL.name())) {
m_isNull = obj.getBoolean(Members.ISNULL.name());
}
}
public static Object extractPartitioningValue(VoltType voltType, AbstractExpression constExpr) {
// TODO: There is currently no way to pass back as a partition key value
// the constant value resulting from a general constant expression such as
// "WHERE a.pk = b.pk AND b.pk = SQRT(3*3+4*4)" because the planner has no expression evaluation capabilities.
if (constExpr instanceof ConstantValueExpression) {
// ConstantValueExpression exports its value as a string, which is handy for serialization,
// but the hashinator wants a partition-key-column-type-appropriate value.
// For safety, don't trust the constant's type
// -- it's apparently comparable to the column, but may not be an exact match(?).
// XXX: Actually, there may need to be additional filtering in the code above to not accept
// constant equality filters that would require the COLUMN type to be non-trivially converted (?)
// -- it MAY not be safe to limit execution of such a filter on any single partition.
// For now, for partitioning purposes, leave constants for string columns as they are,
// and process matches for integral columns via constant-to-string-to-bigInt conversion.
String stringValue = ((ConstantValueExpression) constExpr).getValue();
if (voltType.isBackendIntegerType()) {
try {
return new Long(stringValue);
} catch (NumberFormatException nfe) {
// Disqualify this constant by leaving objValue null -- probably should have caught this earlier?
// This causes the statement to fall back to being identified as multi-partition.
}
} else {
return stringValue;
}
}
return null;
}
/**
* This method will alter the type of this constant expression based on the context
* in which it appears. For example, each constant in the value list of an INSERT
* statement will be refined to the type of the column in the table being inserted into.
*
* Here is a summary of the rules used to convert types here:
* - VARCHAR literals may be reinterpreted as (depending on the type needed):
* - VARBINARY (string is required to have an even number of hex digits)
* - TIMESTAMP (string must have timestamp format)
* - Some numeric type (any of the four integer types, DECIMAL or FLOAT)
*
* In addition, if this object is a VARBINARY constant (e.g., X'00abcd') and we need
* an integer constant, (any of TINYINT, SMALLINT, INTEGER or BIGINT),
* we interpret the hex digits as a 64-bit signed integer. If there are fewer than 16 hex digits,
* the most significant bits are assumed to be zeros. So for example, X'FF' appearing where we want a
* TINYINT would be out-of-range, since it's 255 and not -1.
*
* There is corresponding code for handling integer hex literals in ParameterConverter for parameters,
* and in HSQL's ExpressionValue class.
*/
@Override
public void refineValueType(VoltType neededType, int neededSize)
{
int size_unit = 1;
if (neededType == m_valueType) {
if (neededSize == m_valueSize) {
return;
}
// Variably sized types need to fit within the target width.
if (neededType == VoltType.VARBINARY) {
if ( ! Encoder.isHexEncodedString(getValue())) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
size_unit = 2;
}
else {
assert neededType == VoltType.STRING;
}
if (getValue().length() > size_unit*neededSize ) {
throw new PlanningErrorException("Value (" + getValue() +
") is too wide for a constant " +
neededType.toSQLString() +
" value of size " + neededSize);
}
setValueSize(neededSize);
return;
}
if (m_isNull) {
setValueType(neededType);
setValueSize(neededSize);
return;
}
// Constant's apparent type may not exactly match the target type needed.
if (neededType == VoltType.VARBINARY &&
(m_valueType == VoltType.STRING || m_valueType == null)) {
if ( ! Encoder.isHexEncodedString(getValue())) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
size_unit = 2;
if (getValue().length() > size_unit*neededSize ) {
throw new PlanningErrorException("Value (" + getValue() +
") is too wide for a constant " +
neededType.toSQLString() +
" value of size " + neededSize);
}
setValueType(neededType);
setValueSize(neededSize);
return;
}
if (neededType == VoltType.STRING && m_valueType == null) {
if (getValue().length() > size_unit*neededSize ) {
throw new PlanningErrorException("Value (" + getValue() +
") is too wide for a constant " +
neededType.toSQLString() +
" value of size " + neededSize);
}
setValueType(neededType);
setValueSize(neededSize);
return;
}
if (neededType == VoltType.TIMESTAMP) {
if (m_valueType == VoltType.STRING) {
try {
// Convert date value in whatever format is supported by
// TimeStampType into VoltDB native microsecond count.
// TODO: Should datetime string be supported as the new
// canonical internal format for timestamp constants?
// Historically, the long micros value made sense because
// it was initially the only way and later the most
// direct way to initialize timestamp values in the EE.
// But now that long value can not be used to "explain"
// an expression as a valid SQL timestamp value for DDL
// round trips, forcing a reverse conversion back through
// TimeStampType to a datetime string.
TimestampType ts = new TimestampType(m_value);
m_value = String.valueOf(ts.getTime());
}
// It couldn't be converted to timestamp.
catch (IllegalArgumentException e) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
setValueType(neededType);
setValueSize(neededSize);
return;
}
}
if ((neededType == VoltType.FLOAT || neededType == VoltType.DECIMAL)
&& getValueType() != VoltType.VARBINARY) {
if (m_valueType == null ||
(m_valueType != VoltType.NUMERIC && ! m_valueType.isExactNumeric())) {
try {
Double.parseDouble(getValue());
} catch (NumberFormatException nfe) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
}
setValueType(neededType);
setValueSize(neededSize);
return;
}
if (neededType.isBackendIntegerType()) {
long value = 0;
try {
if (getValueType() == VoltType.VARBINARY) {
value = SQLParser.hexDigitsToLong(getValue());
setValue(Long.toString(value));
}
else {
value = Long.parseLong(getValue());
}
} catch (SQLParser.Exception | NumberFormatException exc) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
checkIntegerValueRange(value, neededType);
m_valueType = neededType;
m_valueSize = neededType.getLengthInBytesForFixedTypes();
return;
}
// That's it for known type conversions.
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
neededType.toSQLString() + " value");
}
private static void checkIntegerValueRange(long value, VoltType integerType) {
// Note that while Long.MIN_VALUE is used to represent NULL in VoltDB, we have decided that
// pass in the literal for Long.MIN_VALUE makes very little sense when you have the option
// to use the literal NULL. Thus the NULL values for each of the 4 integer types are considered
// an underflow exception for the type.
if (integerType == VoltType.BIGINT || integerType == VoltType.TIMESTAMP) {
if (value == VoltType.NULL_BIGINT)
throw new PlanningErrorException("Constant value underflows BIGINT type.");
}
if (integerType == VoltType.INTEGER) {
if ((value > Integer.MAX_VALUE) || (value <= VoltType.NULL_INTEGER))
throw new PlanningErrorException("Constant value overflows/underflows INTEGER type.");
}
if (integerType == VoltType.SMALLINT) {
if ((value > Short.MAX_VALUE) || (value <= VoltType.NULL_SMALLINT))
throw new PlanningErrorException("Constant value overflows/underflows SMALLINT type.");
}
if (integerType == VoltType.TINYINT) {
if ((value > Byte.MAX_VALUE) || (value <= VoltType.NULL_TINYINT))
throw new PlanningErrorException("Constant value overflows/underflows TINYINT type.");
}
}
@Override
public void refineOperandType(VoltType columnType) {
if (m_valueType != VoltType.NUMERIC) {
return;
}
if (columnType == null || columnType == VoltType.NUMERIC) {
return;
}
if ((columnType == VoltType.FLOAT) || (columnType == VoltType.DECIMAL)) {
m_valueType = columnType;
m_valueSize = columnType.getLengthInBytesForFixedTypes();
return;
}
if (columnType.isBackendIntegerType()) {
columnType = VoltTypeUtil.getNumericLiteralType(columnType, getValue());
m_valueType = columnType;
m_valueSize = columnType.getLengthInBytesForFixedTypes();
}
else {
throw new NumberFormatException("NUMERIC constant value type must match a FLOAT, DECIMAL, or integral column, not " + columnType.toSQLString());
}
}
@Override
public void finalizeValueTypes() {
if (m_valueType != VoltType.NUMERIC) {
return;
}
// By default, constants should be treated as DECIMAL other than FLOAT to preserve the precision
// However, the range of DECIMAL of our implementation is small
m_valueType = VoltType.FLOAT;
m_valueSize = m_valueType.getLengthInBytesForFixedTypes();
}
/**
* Tests if the value is a string that would represent a prefix if used as a LIKE pattern.
* The value must end in a '%' and contain no other wildcards ('_' or '%').
**/
public boolean isPrefixPatternString() {
String patternString = getValue();
int length = patternString.length();
if (length == 0) {
return false;
}
// '_' is not allowed.
int disallowedWildcardPos = patternString.indexOf('_');
if (disallowedWildcardPos != -1) {
return false;
}
int firstWildcardPos = patternString. indexOf('%');
// Indexable filters have only a trailing '%'.
// NOTE: not bothering to check for silly synonym patterns with multiple trailing '%'s.
if (firstWildcardPos != length-1) {
return false;
}
return true;
}
@Override
public String explain(String unused) {
if (m_isNull) {
return "NULL";
}
if (m_valueType == VoltType.STRING) {
return "'" + m_value + "'";
}
if (m_valueType == VoltType.TIMESTAMP) {
try {
// Convert the datetime value in its canonical internal form,
// currently a count of epoch microseconds,
// through TimeStampType into a timestamp string.
long micros = Long.valueOf(m_value);
TimestampType ts = new TimestampType(micros);
return "'" + ts.toString() + "'";
}
// It couldn't be converted to timestamp.
catch (IllegalArgumentException e) {
throw new PlanningErrorException("Value (" + getValue() +
") has an invalid format for a constant " +
VoltType.TIMESTAMP.toSQLString() + " value");
}
}
return m_value;
}
/**
* Create a new CVE for a given type and value
* @param dataType
* @param value
* @return
*/
public static ConstantValueExpression makeExpression(VoltType dataType, String value) {
ConstantValueExpression constantExpr = new ConstantValueExpression();
constantExpr.setValueType(dataType);
constantExpr.setValue(value);
return constantExpr;
}
/**
* Create TRUE CVE
* @return
*/
public static ConstantValueExpression getTrue() {
return makeExpression(VoltType.BOOLEAN, Boolean.TRUE.toString());
}
/**
* Create FALSE CVE
* @return
*/
public static ConstantValueExpression getFalse() {
return makeExpression(VoltType.BOOLEAN, Boolean.FALSE.toString());
}
/**
* Return true if and only if an input expression's type is boolean and value is "true"
* @param expr
* @return
*/
public static boolean isBooleanTrue(AbstractExpression expr) {
return isBooleanValue(expr, Boolean.TRUE);
}
/**
* Return true if and only if an input expression's type is boolean and value is "false"
* @param expr
* @return
*/
public static boolean isBooleanFalse(AbstractExpression expr) {
return isBooleanValue(expr, Boolean.FALSE);
}
private static boolean isBooleanValue(AbstractExpression expr, Boolean value) {
if (expr instanceof ConstantValueExpression) {
ConstantValueExpression cve = (ConstantValueExpression) expr;
if (VoltType.BOOLEAN == cve.getValueType()) {
return value.toString().equals(cve.getValue());
}
}
return false;
}
}