All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.sql.impl.calcite.HazelcastSqlToRelConverter Maven / Gradle / Ivy

/*
 * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved.
 *
 * Licensed 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 com.hazelcast.sql.impl.calcite;

import com.hazelcast.sql.impl.QueryException;
import com.hazelcast.sql.impl.SqlErrorCode;
import com.hazelcast.sql.impl.calcite.validate.HazelcastResources;
import com.hazelcast.sql.impl.calcite.validate.literal.Literal;
import com.hazelcast.sql.impl.calcite.validate.literal.LiteralUtils;
import com.hazelcast.sql.impl.calcite.validate.operators.HazelcastReturnTypeInference;
import com.hazelcast.sql.impl.calcite.validate.types.HazelcastTypeUtils;
import com.hazelcast.sql.impl.type.QueryDataType;
import com.hazelcast.sql.impl.type.converter.Converter;
import com.hazelcast.sql.impl.type.converter.Converters;
import com.hazelcast.org.apache.calcite.plan.RelOptCluster;
import com.hazelcast.org.apache.calcite.plan.RelOptTable;
import com.hazelcast.org.apache.calcite.prepare.Prepare;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.rex.RexLiteral;
import com.hazelcast.org.apache.calcite.rex.RexNode;
import com.hazelcast.org.apache.calcite.runtime.CalciteContextException;
import com.hazelcast.org.apache.calcite.runtime.Resources;
import com.hazelcast.org.apache.calcite.sql.SqlCall;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidator;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorException;
import com.hazelcast.org.apache.calcite.sql2rel.SqlRexConvertletTable;
import com.hazelcast.org.apache.calcite.sql2rel.SqlToRelConverter;
import com.hazelcast.org.apache.calcite.util.TimeString;

import java.time.LocalTime;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.CHAR_TYPES;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.NULL;
import static com.hazelcast.org.apache.calcite.sql.type.SqlTypeName.TIME;

/**
 * Custom Hazelcast sql-to-rel converter.
 * 

* Currently, this custom sql-to-rel converter is used to workaround quirks of * the default Calcite sql-to-rel converter and to facilitate generation of * literals and casts with more precise types assigned during the validation. */ public class HazelcastSqlToRelConverter extends SqlToRelConverter { /** See {@link #convertCall(SqlNode, Blackboard)} for more information. */ private final Set callSet = Collections.newSetFromMap(new IdentityHashMap<>()); public HazelcastSqlToRelConverter( RelOptTable.ViewExpander viewExpander, SqlValidator validator, Prepare.CatalogReader catalogReader, RelOptCluster cluster, SqlRexConvertletTable convertletTable, Config config ) { super(viewExpander, validator, catalogReader, cluster, convertletTable, config); } @Override protected RexNode convertExtendedExpression(SqlNode node, Blackboard blackboard) { // Hook into conversion of literals, casts and calls to execute our own logic. if (node.getKind() == SqlKind.LITERAL) { return convertLiteral((SqlLiteral) node); } else if (node.getKind() == SqlKind.CAST) { return convertCast((SqlCall) node, blackboard); } else if (node instanceof SqlCall) { return convertCall(node, blackboard); } return null; } /** * Convert a literal taking into account the type that we assigned to it during validation. * Otherwise Apache Calcite will try to deduce literal type again, leading to incorrect exposed types. *

* For example, {@code [x:BIGINT > 1]} is interpreted as {@code [x:BIGINT > 1:BIGINT]} during the validation. * If this method is not invoked, Apache Calcite will convert it to {[@code x:BIGINT > 1:TINYINT]} instead. */ private RexNode convertLiteral(SqlLiteral literal) { RelDataType type = validator.getValidatedNodeType(literal); return getRexBuilder().makeLiteral(literal.getValue(), type, true); } /** * Convert CAST expression fixing several Apache Calcite problems with literals along the way (see inline JavaDoc). */ private RexNode convertCast(SqlCall call, Blackboard blackboard) { SqlNode operand = call.operand(0); RexNode convertedOperand = blackboard.convertExpression(operand); RelDataType from = validator.getValidatedNodeType(operand); RelDataType to = validator.getValidatedNodeType(call); QueryDataType fromType = HazelcastTypeUtils.toHazelcastType(from.getSqlTypeName()); QueryDataType toType = HazelcastTypeUtils.toHazelcastType(to.getSqlTypeName()); Literal literal = LiteralUtils.literal(convertedOperand); if (literal != null && ((RexLiteral) convertedOperand).getTypeName() != NULL) { // There is a bug in RexBuilder.makeCast(). If the operand is a literal, it can directly return a literal with the // desired target type instead of an actual cast, but when doing that it doesn't check for numeric overflow. // For example if this method is converting [128 AS TINYINT] is converted to -1, which is obviously incorrect. // It should have failed. // To workaround the problem, we perform the conversion using our converters manually. If the conversion fails, // we throw an error (it would have been thrown if the conversion was performed at runtime anyway), before // delegating to RexBuilder.makeCast(). // Since this workaround moves conversion errors to the parsing phase, we conduct the conversion check for all // types to ensure that we throw consistent error messages for all literal-related conversions errors. try { // The literal's type might be different from the operand type for example here: // CAST(CAST(42 AS SMALLINT) AS TINYINT) // The operand of the outer cast is validated as a SMALLINT, however the operand, thanks to the // simplification in RexBuilder.makeCast(), is converted to a literal [42:SMALLINT]. And LiteralUtils converts // this operand to [42:TINYINT] - we have to use the literal's type instead of the validated operand type. QueryDataType actualFromType = HazelcastTypeUtils.toHazelcastType(literal.getTypeName()); toType.getConverter().convertToSelf(actualFromType.getConverter(), literal.getValue()); } catch (Exception e) { throw literalConversionException(validator, call, literal, toType, e); } // Normalize BOOLEAN and DOUBLE literals when converting them to VARCHAR. // BOOLEAN literals are converted to "true"/"false" instead of "TRUE"/"FALSE". // DOUBLE literals are converted to a string with scientific conventions (e.g., 1.1E1 instead of 11.0); if (CHAR_TYPES.contains(to.getSqlTypeName())) { return getRexBuilder().makeLiteral(literal.getStringValue(), to, true); } // There is a bug in RexSimplify that adds an unnecessary second. For example, the string literal "00:00" is // converted to 00:00:01. The problematic code is located in DateTimeUtils.timeStringToUnixDate. // To workaround the problem, we perform the conversion manually. if (CHAR_TYPES.contains(from.getSqlTypeName()) && to.getSqlTypeName() == TIME) { LocalTime time = fromType.getConverter().asTime(literal.getStringValue()); TimeString timeString = new TimeString(time.getHour(), time.getMinute(), time.getSecond()); return getRexBuilder().makeLiteral(timeString, to, true); } // Apache Calcite uses an expression simplification logic that treats CASTs with inexact literals incorrectly. // For example, "CAST(1.0 as DOUBLE) = CAST(1.0000000000000001 as DOUBLE)" is converted to "false", while it should // be "true". See CastFunctionIntegrationTest.testApproximateTypeSimplification - it will fail without this fix. if (fromType.getTypeFamily().isNumeric()) { if (toType.getTypeFamily().isNumericApproximate()) { Converter converter = Converters.getConverter(literal.getValue().getClass()); Object convertedValue = toType.getConverter().convertToSelf(converter, literal.getValue()); return getRexBuilder().makeLiteral(convertedValue, to, false); } } } // Delegate to Apache Calcite. return getRexBuilder().makeCast(to, convertedOperand); } /** * This method overcomes a bug in Apache Calcite that ignores previously resolved return types of the expression * and instead attempts to infer them again using a different logic. Without this fix, we will get type resolution * errors after a SQL-to-rel conversion. *

* The method relies on the fact that all operators use {@link HazelcastReturnTypeInference} as a top-level return type * inference method. *

    *
  • When a call node is observed for the first time, get its return type and save it to a thread-local variable *
  • Then delegate back to original converter code *
  • When converter attempts to resolve the return type of a call, it will get the previously saved type from * the thread-local variable *
*/ private RexNode convertCall(SqlNode node, Blackboard blackboard) { // Ignore DEFAULT (used for default function arguments). This node isn't // present in the original parse tree and at the validation time, but is // added later when converting by SqlCallBinding.permutedCall(), which // adjusts the actual function arguments to the formal arguments, adding // this node for the missing arguments. If we called getValidatedNodeType() // for DEFAULT node, it will fail. if (node.getKind() == SqlKind.DEFAULT) { return null; } if (callSet.add(node)) { try { RelDataType type = validator.getValidatedNodeType(node); HazelcastReturnTypeInference.push(type); try { return blackboard.convertExpression(node); } finally { HazelcastReturnTypeInference.pop(); } } finally { callSet.remove(node); } } return null; } private static QueryException literalConversionException( SqlValidator validator, SqlCall call, Literal literal, QueryDataType toType, Exception e ) { String literalValue = literal.getStringValue(); if (CHAR_TYPES.contains(literal.getTypeName())) { literalValue = "'" + literalValue + "'"; } Resources.ExInst contextError = HazelcastResources.RESOURCES.cannotCastLiteralValue( literalValue, toType.getTypeFamily().getPublicType().name(), e.getMessage() ); CalciteContextException calciteContextError = validator.newValidationError(call, contextError); throw QueryException.error(SqlErrorCode.PARSING, calciteContextError.getMessage(), e); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy