com.hazelcast.jet.sql.impl.validate.types.HazelcastTypeCoercion Maven / Gradle / Ivy
/*
* Copyright 2021 Hazelcast Inc.
*
* Licensed under the Hazelcast Community License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://hazelcast.com/hazelcast-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.jet.sql.impl.validate.types;
import com.hazelcast.jet.sql.impl.validate.HazelcastSqlOperatorTable;
import com.hazelcast.jet.sql.impl.validate.HazelcastSqlValidator;
import com.hazelcast.sql.impl.type.QueryDataType;
import com.hazelcast.sql.impl.type.QueryDataTypeFamily;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeField;
import com.hazelcast.org.apache.calcite.sql.SqlBasicCall;
import com.hazelcast.org.apache.calcite.sql.SqlCall;
import com.hazelcast.org.apache.calcite.sql.SqlCallBinding;
import com.hazelcast.org.apache.calcite.sql.SqlDataTypeSpec;
import com.hazelcast.org.apache.calcite.sql.SqlFunction;
import com.hazelcast.org.apache.calcite.sql.SqlInsert;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlSelect;
import com.hazelcast.org.apache.calcite.sql.SqlUpdate;
import com.hazelcast.org.apache.calcite.sql.SqlUserDefinedTypeNameSpec;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserPos;
import com.hazelcast.org.apache.calcite.sql.type.SqlTypeFamily;
import com.hazelcast.org.apache.calcite.sql.type.SqlTypeUtil;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorScope;
import com.hazelcast.org.apache.calcite.sql.validate.implicit.TypeCoercionImpl;
import java.util.List;
import java.util.function.Consumer;
import static com.hazelcast.sql.impl.type.QueryDataType.OBJECT;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.ANY;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.NULL;
import static com.hazelcast.org.apache.calcite.util.Static.RESOURCE;
/**
* Provides custom coercion strategies supporting {@link HazelcastIntegerType}
* and assigning more precise types comparing to the standard Calcite coercion.
*/
public final class HazelcastTypeCoercion extends TypeCoercionImpl {
public HazelcastTypeCoercion(HazelcastSqlValidator validator) {
super(HazelcastTypeFactory.INSTANCE, validator);
}
@Override
public boolean coerceOperandType(SqlValidatorScope scope, SqlCall call, int index, RelDataType targetType) {
SqlNode operand = call.operand(index);
return coerceNode(scope, operand, targetType, cast -> call.setOperand(index, cast));
}
private boolean coerceNode(
SqlValidatorScope scope,
SqlNode node,
RelDataType targetType,
Consumer replaceFn
) {
// Just update the inferred type if casting is not needed
if (!requiresCast(scope, node, targetType)) {
updateInferredType(node, targetType);
return false;
}
SqlDataTypeSpec targetTypeSpec;
if (targetType instanceof HazelcastIntegerType) {
targetTypeSpec = new SqlDataTypeSpec(
new HazelcastIntegerTypeNameSpec((HazelcastIntegerType) targetType),
SqlParserPos.ZERO
);
} else {
if (targetType.getSqlTypeName() == ANY) {
// without this the subsequent call to UnsupportedOperationVerifier will fail with "we do not support ANY"
targetTypeSpec =
new SqlDataTypeSpec(new SqlUserDefinedTypeNameSpec("OBJECT", SqlParserPos.ZERO), SqlParserPos.ZERO)
.withNullable(targetType.isNullable());
} else {
targetTypeSpec = SqlTypeUtil.convertTypeToSpec(targetType);
}
}
SqlNode cast = cast(node, targetTypeSpec);
replaceFn.accept(cast);
validator.deriveType(scope, cast);
return true;
}
private static SqlNode cast(SqlNode node, SqlDataTypeSpec targetTypeSpec) {
if (node.getKind() == SqlKind.ARGUMENT_ASSIGNMENT) {
// transform name => 'value' into name => cast('value')
SqlBasicCall call = (SqlBasicCall) node;
SqlNode value = call.getOperandList().get(0);
SqlNode name = call.getOperandList().get(1);
SqlNode cast = cast(value, targetTypeSpec);
return call.getOperator().createCall(SqlParserPos.ZERO, cast, name);
} else {
return HazelcastSqlOperatorTable.CAST.createCall(SqlParserPos.ZERO, node, targetTypeSpec);
}
}
private boolean requiresCast(SqlValidatorScope scope, SqlNode node, RelDataType to) {
RelDataType from = validator.deriveType(scope, node);
if (from.getSqlTypeName() == NULL || SqlUtil.isNullLiteral(node, false) || node.getKind() == SqlKind.DYNAMIC_PARAM) {
// Never cast NULLs or dynamic params, just assign types to them
return false;
}
// CAST is only required between different types.
return from.getSqlTypeName() != to.getSqlTypeName();
}
@Override
public boolean binaryArithmeticCoercion(SqlCallBinding binding) {
throw new UnsupportedOperationException("Should not be called");
}
@Override
public boolean binaryComparisonCoercion(SqlCallBinding binding) {
throw new UnsupportedOperationException("Should not be called");
}
/**
* {@inheritDoc}
*
* We change the contract of the superclass' return type. According to the
* superclass contract we're supposed to return true iff we successfully
* added a CAST. This method returns true if the expression can now be
* assigned to {@code targetType}, either because a CAST was added, or
* because it already was assignable (e.g. the type was same). This is
* needed for {@link #querySourceCoercion} method, which calls this method.
*
* @return True, if the source column can now be assigned to {@code
* targetType}
*/
@Override
public boolean rowTypeCoercion(SqlValidatorScope scope, SqlNode query, int columnIndex, RelDataType targetType) {
switch (query.getKind()) {
case SELECT:
SqlSelect selectNode = (SqlSelect) query;
SqlValidatorScope selectScope = validator.getSelectScope(selectNode);
if (!rowTypeElementCoercion(selectScope, selectNode.getSelectList().get(columnIndex), targetType,
newNode -> selectNode.getSelectList().set(columnIndex, newNode))) {
return false;
}
updateInferredColumnType(selectScope, query, columnIndex, targetType);
return true;
case VALUES:
for (SqlNode rowConstructor : ((SqlCall) query).getOperandList()) {
if (!rowTypeElementCoercion(scope, ((SqlCall) rowConstructor).operand(columnIndex), targetType,
newNode -> ((SqlCall) rowConstructor).setOperand(columnIndex, newNode))) {
return false;
}
}
updateInferredColumnType(scope, query, columnIndex, targetType);
return true;
default:
throw new UnsupportedOperationException("unexpected: " + query.getKind());
}
}
public boolean rowTypeElementCoercion(
SqlValidatorScope scope,
SqlNode rowElement,
RelDataType targetType,
Consumer replaceFn
) {
if (targetType.equals(scope.getValidator().getUnknownType())) {
return false;
}
RelDataType sourceType = validator.deriveType(scope, rowElement);
QueryDataType sourceHzType = HazelcastTypeUtils.toHazelcastType(sourceType.getSqlTypeName());
QueryDataType targetHzType = HazelcastTypeUtils.toHazelcastType(targetType.getSqlTypeName());
if (sourceHzType.getTypeFamily() == targetHzType.getTypeFamily() || targetHzType == OBJECT) {
// Do nothing.
return true;
}
boolean valid = sourceAndTargetAreNumeric(targetHzType, sourceHzType)
|| sourceAndTargetAreTemporalAndSourceCanBeConvertedToTarget(targetHzType, sourceHzType)
|| targetIsTemporalAndSourceIsVarcharLiteral(targetHzType, sourceHzType, rowElement)
|| sourceHzType.getTypeFamily() == QueryDataTypeFamily.NULL;
if (!valid) {
// Types cannot be converted to each other, fail to coerce
return false;
}
// Types are in the same group, cast source to target.
coerceNode(scope, rowElement, targetType, replaceFn);
return true;
}
private static boolean sourceAndTargetAreNumeric(QueryDataType highHZType, QueryDataType lowHZType) {
return (highHZType.getTypeFamily().isNumeric() && lowHZType.getTypeFamily().isNumeric());
}
private static boolean sourceAndTargetAreTemporalAndSourceCanBeConvertedToTarget(QueryDataType targetHzType,
QueryDataType sourceHzType) {
return targetHzType.getTypeFamily().isTemporal()
&& sourceHzType.getTypeFamily().isTemporal()
&& sourceHzType.getConverter().canConvertTo(targetHzType.getTypeFamily());
}
private static boolean targetIsTemporalAndSourceIsVarcharLiteral(QueryDataType targetHzType,
QueryDataType sourceHzType, SqlNode sourceNode) {
SqlKind sourceKind = sourceNode.getKind();
if (sourceKind == SqlKind.AS) {
return targetIsTemporalAndSourceIsVarcharLiteral(
targetHzType,
sourceHzType,
((SqlBasicCall) sourceNode).operand(0)
);
} else {
return targetHzType.getTypeFamily().isTemporal()
&& sourceHzType.getTypeFamily() == QueryDataTypeFamily.VARCHAR
&& sourceKind == SqlKind.LITERAL;
}
}
@Override
public boolean caseWhenCoercion(SqlCallBinding callBinding) {
throw new UnsupportedOperationException("Should not be called");
}
@Override
public boolean builtinFunctionCoercion(
SqlCallBinding binding,
List operandTypes,
List expectedFamilies
) {
throw new UnsupportedOperationException("Should not be called");
}
@Override
public boolean userDefinedFunctionCoercion(SqlValidatorScope scope, SqlCall call, SqlFunction function) {
throw new UnsupportedOperationException("Should not be called");
}
@Override
public boolean querySourceCoercion(
SqlValidatorScope scope,
RelDataType sourceRowType,
RelDataType targetRowType,
SqlNode query
) {
// the code below copied from superclass implementation, but uses our `canCast` method
final List sourceFields = sourceRowType.getFieldList();
final List targetFields = targetRowType.getFieldList();
assert sourceFields.size() == targetFields.size();
final int fieldCount = sourceFields.size();
for (int i = 0; i < fieldCount; i++) {
RelDataType sourceType = sourceFields.get(i).getType();
RelDataType targetType = targetFields.get(i).getType();
if (!SqlTypeUtil.equalSansNullability(validator.getTypeFactory(), sourceType, targetType)
&& !HazelcastTypeUtils.canCast(sourceType, targetType)
|| !coerceSourceRowType(scope, query, i, fieldCount, targetType)) {
SqlNode node = getNthExpr(query, i, fieldCount);
throw scope.getValidator().newValidationError(node,
RESOURCE.typeNotAssignable(
targetFields.get(i).getName(), targetType.toString(),
sourceFields.get(i).getName(), sourceType.toString()));
}
}
// We always return true to defuse the fallback mechanism in the caller.
// Instead, we throw the validation error ourselves above if we can't assign.
return true;
}
/**
* Copied from {@code com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorImpl#getNthExpr()}.
*
* Locates the n-th expression in an INSERT or UPDATE query.
*
* @param query Query
* @param ordinal Ordinal of expression
* @param sourceCount Number of expressions
* @return Ordinal'th expression, never null
*/
private SqlNode getNthExpr(SqlNode query, int ordinal, int sourceCount) {
if (query instanceof SqlInsert) {
SqlInsert insert = (SqlInsert) query;
if (insert.getTargetColumnList() != null) {
return insert.getTargetColumnList().get(ordinal);
} else {
return getNthExpr(
insert.getSource(),
ordinal,
sourceCount);
}
} else if (query instanceof SqlUpdate) {
// trailing elements of selectList are equal to elements of sourceExpressionList
// see JetSqlValidator.validateUpdate()
SqlUpdate update = (SqlUpdate) query;
SqlNodeList selectList = update.getSourceSelect().getSelectList();
return selectList.get(selectList.size() - sourceCount + ordinal);
} else if (query instanceof SqlSelect) {
SqlSelect select = (SqlSelect) query;
if (select.getSelectList().size() == sourceCount) {
return select.getSelectList().get(ordinal);
} else {
// give up
return query;
}
} else {
// give up
return query;
}
}
// originally copied from TypeCoercionImpl
private boolean coerceSourceRowType(
SqlValidatorScope sourceScope,
SqlNode query,
int columnIndex,
int totalColumns,
RelDataType targetType
) {
switch (query.getKind()) {
case INSERT:
SqlInsert insert = (SqlInsert) query;
return coerceSourceRowType(sourceScope,
insert.getSource(),
columnIndex,
totalColumns,
targetType);
case UPDATE:
// trailing elements of selectList are equal to elements of sourceExpressionList
// see JetSqlValidator.validateUpdate()
SqlUpdate update = (SqlUpdate) query;
SqlNodeList selectList = update.getSourceSelect().getSelectList();
return coerceSourceRowType(sourceScope, selectList, selectList.size() - totalColumns + columnIndex, targetType);
default:
return rowTypeCoercion(sourceScope, query, columnIndex, targetType);
}
}
private boolean coerceSourceRowType(SqlValidatorScope scope, SqlNodeList nodeList, int index, RelDataType targetType) {
SqlNode node = nodeList.get(index);
return rowTypeElementCoercion(scope, node, targetType, cast -> nodeList.set(index, cast));
}
}