com.hazelcast.sql.impl.calcite.validate.types.HazelcastTypeSystem Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in com.hazelcast.com.liance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.com.hazelcast.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 com.hazelcast.com.hazelcast.sql.impl.calcite.validate.types;
import com.hazelcast.com.hazelcast.sql.impl.QueryException;
import com.hazelcast.com.hazelcast.sql.impl.SqlErrorCode;
import com.hazelcast.com.hazelcast.sql.impl.calcite.SqlToQueryType;
import com.hazelcast.com.hazelcast.sql.impl.calcite.validate.HazelcastSqlValidator;
import com.hazelcast.com.hazelcast.sql.impl.type.QueryDataType;
import com.hazelcast.com.hazelcast.sql.impl.type.QueryDataTypeFamily;
import com.hazelcast.com.hazelcast.sql.impl.type.converter.Converter;
import com.hazelcast.com.hazelcast.sql.impl.type.converter.Converters;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeFactory;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeSystem;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeSystemImpl;
import com.hazelcast.org.apache.calcite.sql.SqlIdentifier;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.type.BasicSqlType;
import com.hazelcast.org.apache.calcite.sql.type.SqlTypeName;
import java.math.BigDecimal;
import java.util.Calendar;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.APPROX_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.BIGINT;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.CHAR_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.DATETIME_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.DAY_INTERVAL_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.DECIMAL;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.DOUBLE;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.FRACTIONAL_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.INTERVAL_DAY_SECOND;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.INTERVAL_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.INTERVAL_YEAR_MONTH;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.INT_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.NUMERIC_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.YEAR_INTERVAL_TYPES;
/**
* Custom Hazelcast type system.
*
* Overrides some properties of the default Calcite type system, like maximum
* numeric precision, and provides various type-related utilities for {@link
* HazelcastTypeCoercion} and {@link HazelcastSqlValidator}.
*/
public final class HazelcastTypeSystem extends RelDataTypeSystemImpl {
/**
* Shared Hazelcast type system instance.
*/
public static final RelDataTypeSystem INSTANCE = new HazelcastTypeSystem();
/**
* Defines maximum DECIMAL precision.
*/
public static final int MAX_DECIMAL_PRECISION = QueryDataType.MAX_DECIMAL_PRECISION;
/**
* Defines maximum DECIMAL scale.
*/
public static final int MAX_DECIMAL_SCALE = MAX_DECIMAL_PRECISION;
private HazelcastTypeSystem() {
// No-op
}
/**
* @return {@code true} if the given identifier specifies OBJECT type,
* {@code false} otherwise.
*/
public static boolean isObject(SqlIdentifier identifier) {
return identifier.isSimple() && QueryDataTypeFamily.OBJECT.name().equalsIgnoreCase(identifier.getSimple());
}
public static boolean isTimestampWithTimeZone(SqlIdentifier identifier) {
return identifier.isSimple()
&& QueryDataTypeFamily.TIMESTAMP_WITH_TIME_ZONE.name().equalsIgnoreCase(identifier.getSimple());
}
/**
* Determines is it possible to cast from one type to another or not.
*
* @param from the type to cast from.
* @param to the type to cast to.
* @return {@code true} if the cast is possible, {@code false} otherwise.
*/
public static boolean canCast(RelDataType from, RelDataType to) {
QueryDataType queryFrom = SqlToQueryType.map(typeName(from));
QueryDataType queryTo = SqlToQueryType.map(typeName(to));
return queryFrom.getConverter().canConvertTo(queryTo.getTypeFamily());
}
/**
* Determines if it is possible to represent the given literal as a value of
* the given target type.
*
* @param literal the literal in question.
* @param target the target type to represent the literal value as.
* @return {@code true} if the type of given literal is com.hazelcast.com.atible with
* the given target type and the value of given literal is convertible to
* the given target type, {@code false} otherwise.
*/
public static boolean canRepresent(SqlLiteral literal, RelDataType target) {
if (literal.getTypeName() == target.getSqlTypeName()) {
return true;
}
return canConvert(literalValue(literal), literalType(literal), target);
}
/**
* Determines is it possible to convert the given value from its given type
* to another type.
*
* @param value the value in question.
* @param from the type of the value.
* @param to the target type to convert to.
* @return {@code true} if the type of given value is com.hazelcast.com.atible with
* the given target type and the value is convertible to the given target
* type, {@code false} otherwise.
*/
public static boolean canConvert(Object value, RelDataType from, RelDataType to) {
QueryDataType queryFrom = SqlToQueryType.map(typeName(from));
QueryDataType queryTo = SqlToQueryType.map(typeName(to));
Converter fromConverter = queryFrom.getConverter();
Converter toConverter = queryTo.getConverter();
if (!fromConverter.canConvertTo(queryTo.getTypeFamily())) {
return false;
}
if (value == null) {
// nulls are convertible to any type
return true;
}
Converter valueConverter = Converters.getConverter(value.getClass());
// Convert literal value to 'from' type and then to 'to' type.
Object fromValue;
try {
fromValue = fromConverter.convertToSelf(valueConverter, value);
toConverter.convertToSelf(fromConverter, fromValue);
} catch (QueryException e) {
assert e.getCode() == SqlErrorCode.DATA_EXCEPTION;
return false;
}
return true;
}
/**
* Selects a type having a higher precedence from the two given types.
*
* Type precedence is used to determine resulting types of operators and
* to assign types to their operands.
*
* @param type1 the first type.
* @param type2 the second type.
* @return the type with the higher precedence.
*/
public static RelDataType withHigherPrecedence(RelDataType type1, RelDataType type2) {
int precedence1 = precedenceOf(type1);
int precedence2 = precedenceOf(type2);
assert precedence1 != precedence2 || type1.getSqlTypeName() == type2.getSqlTypeName();
if (precedence1 == precedence2 && isInteger(type1) && isInteger(type2)) {
int bitWidth1 = HazelcastIntegerType.bitWidthOf(type1);
int bitWidth2 = HazelcastIntegerType.bitWidthOf(type2);
return bitWidth1 > bitWidth2 ? type1 : type2;
}
return precedence1 > precedence2 ? type1 : type2;
}
/**
* Selects the narrowest possible numeric type for the given {@link
* Number} value with a potential fallback to the given other type if
* that given other type is a floating-point type.
*
* The given {@link Number} value can be either {@link BigDecimal}, in this
* case the value is considered to be exact (see {@link
* SqlTypeName#EXACT_TYPES}), or {@link Double}, in this case the value is
* considered to be approximate (see {@link SqlTypeName#APPROX_TYPES}).
*
* If the given value is exact, the narrowest integer type is selected if the
* value can be represented without losses as TINYINT, SMALLINT, INTEGER or
* BIGINT. Otherwise, the given fallback type is selected if it's a
* floating-point type. If the given fallback type is not a floating-point
* type, BIGINT is selected for integer values and DECIMAL is selected for
* floating-point values.
*
* If the given value is approximate, the given fallback type is selected if
* the type is REAL; otherwise, DOUBLE is selected.
*
* The method performs only the narrowest type selection and doesn't
* validate the value itself.
*
* @param value the value to select the narrowest type for.
* @param otherType the other fallback type.
* @return the narrowest selected type.
*/
public static RelDataType narrowestTypeFor(Number value, SqlTypeName otherType) {
if (value instanceof BigDecimal) {
BigDecimal decimalValue = (BigDecimal) value;
if (decimalValue.scale() <= 0) {
try {
long longValue = decimalValue.longValueExact();
int bitWidth = HazelcastIntegerType.bitWidthOf(longValue);
return HazelcastIntegerType.of(bitWidth, false);
} catch (ArithmeticException e) {
return HazelcastTypeFactory.INSTANCE.createSqlType(FRACTIONAL_TYPES.contains(otherType) ? otherType : BIGINT);
}
} else {
return HazelcastTypeFactory.INSTANCE.createSqlType(APPROX_TYPES.contains(otherType) ? otherType : DECIMAL);
}
} else {
assert value instanceof Double;
return HazelcastTypeFactory.INSTANCE.createSqlType(APPROX_TYPES.contains(otherType) ? otherType : DOUBLE);
}
}
/**
* @return the SQL type name of the given type.
*/
public static SqlTypeName typeName(RelDataType type) {
return type.getSqlTypeName();
}
/**
* @return {@code true} if the given type is a numeric type, {@code false}
* otherwise.
*
* Numeric types are: TINYINT, SMALLINT, INTEGER, BIGINT, REAL, DOUBLE and
* DECIMAL.
*/
public static boolean isNumeric(RelDataType type) {
return NUMERIC_TYPES.contains(typeName(type));
}
/**
* @return {@code true} if the given type is a char type, {@code false}
* otherwise.
*
* Char types are: CHAR and VARCHAR.
*/
public static boolean isChar(RelDataType type) {
return CHAR_TYPES.contains(typeName(type));
}
/**
* @return {@code true} if the given type is a floating-point type, {@code
* false} otherwise.
*
* Floating-point types are: REAL, DOUBLE and DECIMAL.
*/
public static boolean isFloatingPoint(RelDataType type) {
return FRACTIONAL_TYPES.contains(typeName(type));
}
/**
* @return {@code true} if the given type is an integer type, {@code false}
* otherwise.
*
* Integer types are: TINYINT, SMALLINT, INTEGER and BIGINT.
*/
public static boolean isInteger(RelDataType type) {
return INT_TYPES.contains(type.getSqlTypeName());
}
/**
* @return {@code true} if the given type is a temporal type, {@code false}
* otherwise.
*
* Temporal types are: all variants of DATE, TIME, TIMESTAMP and all variants
* of interval types.
*/
public static boolean isTemporal(RelDataType type) {
return DATETIME_TYPES.contains(typeName(type)) || INTERVAL_TYPES.contains(typeName(type));
}
@Override
public int getMaxNumericPrecision() {
return MAX_DECIMAL_PRECISION;
}
@Override
public int getMaxNumericScale() {
return MAX_DECIMAL_SCALE;
}
private static int precedenceOf(RelDataType type) {
SqlTypeName typeName = type.getSqlTypeName();
if (YEAR_INTERVAL_TYPES.contains(typeName)) {
typeName = INTERVAL_YEAR_MONTH;
} else if (DAY_INTERVAL_TYPES.contains(typeName)) {
typeName = INTERVAL_DAY_SECOND;
}
QueryDataType hzType = SqlToQueryType.map(typeName);
return hzType.getTypeFamily().getPrecedence();
}
@SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:ReturnCount"})
private static Object literalValue(SqlLiteral literal) {
switch (literal.getTypeName()) {
case VARCHAR:
case CHAR:
return literal.getValueAs(String.class);
case BOOLEAN:
return literal.getValueAs(Boolean.class);
case TINYINT:
return literal.getValueAs(Byte.class);
case SMALLINT:
return literal.getValueAs(Short.class);
case INTEGER:
return literal.getValueAs(Integer.class);
case BIGINT:
// XXX: Calcite returns unscaled value of the internally stored
// BigDecimal if a long value is requested on the literal.
BigDecimal decimalValue = literal.getValueAs(BigDecimal.class);
return decimalValue == null ? null : decimalValue.longValue();
case DECIMAL:
return literal.getValueAs(BigDecimal.class);
case REAL:
return literal.getValueAs(Float.class);
case DOUBLE:
return literal.getValueAs(Double.class);
case TIME:
case TIME_WITH_LOCAL_TIME_ZONE:
case DATE:
case TIMESTAMP:
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
return literal.getValueAs(Calendar.class);
case ANY:
return literal.getValueAs(Object.class);
case NULL:
return null;
case SYMBOL:
return literal.getValue();
default:
throw new IllegalArgumentException("unexpected literal type: " + literal.getTypeName());
}
}
private static RelDataType literalType(SqlLiteral literal) {
return HazelcastTypeFactory.INSTANCE.createSqlType(literal.getTypeName());
}
@Override
public RelDataType deriveSumType(RelDataTypeFactory typeFactory, RelDataType argumentType) {
if (argumentType instanceof BasicSqlType) {
SqlTypeName type = deriveSumType(argumentType.getSqlTypeName());
if (type.allowsPrec() && argumentType.getPrecision() != RelDataType.PRECISION_NOT_SPECIFIED) {
int precision = typeFactory.getTypeSystem().getMaxPrecision(type);
if (type.allowsScale()) {
return typeFactory.createTypeWithNullability(
typeFactory.createSqlType(type, precision, argumentType.getScale()),
argumentType.isNullable()
);
} else {
return typeFactory.createTypeWithNullability(
typeFactory.createSqlType(type, precision),
argumentType.isNullable()
);
}
} else {
return typeFactory.createTypeWithNullability(
typeFactory.createSqlType(type),
argumentType.isNullable()
);
}
}
return argumentType;
}
private static SqlTypeName deriveSumType(SqlTypeName type) {
switch (type) {
case TINYINT:
case SMALLINT:
case INTEGER:
case BIGINT:
return SqlTypeName.BIGINT;
case DECIMAL:
return SqlTypeName.DECIMAL;
case REAL:
case DOUBLE:
return SqlTypeName.DOUBLE;
default:
return type;
}
}
@Override
public RelDataType deriveAvgAggType(RelDataTypeFactory typeFactory, RelDataType argumentType) {
switch (argumentType.getSqlTypeName()) {
case TINYINT:
case SMALLINT:
case INTEGER:
case BIGINT:
case DECIMAL:
return typeFactory.createTypeWithNullability(
typeFactory.createSqlType(DECIMAL),
argumentType.isNullable()
);
case REAL:
case DOUBLE:
return typeFactory.createTypeWithNullability(
typeFactory.createSqlType(DOUBLE),
argumentType.isNullable()
);
default:
return argumentType;
}
}
}