com.hazelcast.org.apache.calcite.sql.fun.SqlCaseOperator Maven / Gradle / Ivy
/*
* 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 com.hazelcast.org.apache.calcite.sql.fun;
import com.hazelcast.org.apache.calcite.rel.type.RelDataType;
import com.hazelcast.org.apache.calcite.rel.type.RelDataTypeFactory;
import com.hazelcast.org.apache.calcite.rex.RexCall;
import com.hazelcast.org.apache.calcite.rex.RexCallBinding;
import com.hazelcast.org.apache.calcite.rex.RexNode;
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.SqlKind;
import com.hazelcast.org.apache.calcite.sql.SqlLiteral;
import com.hazelcast.org.apache.calcite.sql.SqlNode;
import com.hazelcast.org.apache.calcite.sql.SqlNodeList;
import com.hazelcast.org.apache.calcite.sql.SqlOperandCountRange;
import com.hazelcast.org.apache.calcite.sql.SqlOperator;
import com.hazelcast.org.apache.calcite.sql.SqlOperatorBinding;
import com.hazelcast.org.apache.calcite.sql.SqlSyntax;
import com.hazelcast.org.apache.calcite.sql.SqlUtil;
import com.hazelcast.org.apache.calcite.sql.SqlWriter;
import com.hazelcast.org.apache.calcite.sql.parser.SqlParserPos;
import com.hazelcast.org.apache.calcite.sql.type.InferTypes;
import com.hazelcast.org.apache.calcite.sql.type.SqlOperandCountRanges;
import com.hazelcast.org.apache.calcite.sql.type.SqlTypeUtil;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidator;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorImpl;
import com.hazelcast.org.apache.calcite.sql.validate.SqlValidatorScope;
import com.hazelcast.org.apache.calcite.sql.validate.implicit.TypeCoercion;
import com.hazelcast.org.apache.calcite.util.Litmus;
import com.hazelcast.org.apache.calcite.util.Pair;
import com.hazelcast.com.google.common.collect.Iterables;
import com.hazelcast.org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.hazelcast.org.apache.calcite.util.Static.RESOURCE;
import static java.util.Objects.requireNonNull;
/**
* An operator describing a CASE
, NULLIF
or
* COALESCE
expression. All of these forms are normalized at parse time
* to a to a simple CASE
statement like this:
*
* CASE
* WHEN <when expression_0> THEN <then expression_0>
* WHEN <when expression_1> THEN <then expression_1>
* ...
* WHEN <when expression_N> THEN <then expression_N>
* ELSE <else expression>
* END
*
* The switched form of the CASE
statement is normalized to the
* simple form by inserting calls to the =
operator. For
* example,
*
* CASE x + y
* WHEN 1 THEN 'fee'
* WHEN 2 THEN 'fie'
* ELSE 'foe'
* END
*
* becomes
*
* CASE
* WHEN Equals(x + y, 1) THEN 'fee'
* WHEN Equals(x + y, 2) THEN 'fie'
* ELSE 'foe'
* END
*
* REVIEW jhyde 2004/3/19 Does Equals
handle NULL semantics
* correctly?
*
* COALESCE(x, y, z)
becomes
*
* CASE
* WHEN x IS NOT NULL THEN x
* WHEN y IS NOT NULL THEN y
* ELSE z
* END
*
* NULLIF(x, -1)
becomes
*
* CASE
* WHEN x = -1 THEN NULL
* ELSE x
* END
*
* Note that some of these normalizations cause expressions to be duplicated.
* This may make it more difficult to write optimizer rules (because the rules
* will have to deduce that expressions are equivalent). It also requires that
* some part of the planning process (probably the generator of the calculator
* program) does common sub-expression elimination.
*
* REVIEW jhyde 2004/3/19. Expanding expressions at parse time has some other
* drawbacks. It is more difficult to give meaningful validation errors: given
* COALESCE(DATE '2004-03-18', 3.5)
, do we issue a type-checking
* error against a CASE
operator? Second, I'd like to use the
* {@link SqlNode} object model to generate SQL to send to 3rd-party databases,
* but there's now no way to represent a call to COALESCE or NULLIF. All in all,
* it would be better to have operators for COALESCE, NULLIF, and both simple
* and switched forms of CASE, then translate to simple CASE when building the
* {@link com.hazelcast.org.apache.calcite.rex.RexNode} tree.
*
* The arguments are physically represented as follows:
*
*
* - The when expressions are stored in a {@link SqlNodeList}
* whenList.
* - The then expressions are stored in a {@link SqlNodeList}
* thenList.
* - The else expression is stored as a regular {@link SqlNode}.
*
*/
public class SqlCaseOperator extends SqlOperator {
public static final SqlCaseOperator INSTANCE = new SqlCaseOperator();
//~ Constructors -----------------------------------------------------------
private SqlCaseOperator() {
super("CASE", SqlKind.CASE, MDX_PRECEDENCE, true, null,
InferTypes.RETURN_TYPE, null);
}
//~ Methods ----------------------------------------------------------------
@Override public void validateCall(
SqlCall call,
SqlValidator validator,
SqlValidatorScope scope,
SqlValidatorScope operandScope) {
final SqlCase sqlCase = (SqlCase) call;
final SqlNodeList whenOperands = sqlCase.getWhenOperands();
final SqlNodeList thenOperands = sqlCase.getThenOperands();
final SqlNode elseOperand = sqlCase.getElseOperand();
for (SqlNode operand : whenOperands) {
operand.validateExpr(validator, operandScope);
}
for (SqlNode operand : thenOperands) {
operand.validateExpr(validator, operandScope);
}
if (elseOperand != null) {
elseOperand.validateExpr(validator, operandScope);
}
}
@Override public RelDataType deriveType(
SqlValidator validator,
SqlValidatorScope scope,
SqlCall call) {
// Do not try to derive the types of the operands. We will do that
// later, top down.
return validateOperands(validator, scope, call);
}
@Override public boolean checkOperandTypes(
SqlCallBinding callBinding,
boolean throwOnFailure) {
SqlCase caseCall = (SqlCase) callBinding.getCall();
SqlNodeList whenList = caseCall.getWhenOperands();
SqlNodeList thenList = caseCall.getThenOperands();
assert whenList.size() == thenList.size();
// checking that search conditions are ok...
for (SqlNode node : whenList) {
// should throw validation error if something wrong...
RelDataType type = SqlTypeUtil.deriveType(callBinding, node);
if (!SqlTypeUtil.inBooleanFamily(type)) {
if (throwOnFailure) {
throw callBinding.newError(RESOURCE.expectedBoolean());
}
return false;
}
}
boolean foundNotNull = false;
for (SqlNode node : thenList) {
if (!SqlUtil.isNullLiteral(node, false)) {
foundNotNull = true;
}
}
if (!SqlUtil.isNullLiteral(
caseCall.getElseOperand(),
false)) {
foundNotNull = true;
}
if (!foundNotNull) {
// according to the sql standard we can not have all of the THEN
// statements and the ELSE returning null
if (throwOnFailure && !callBinding.isTypeCoercionEnabled()) {
throw callBinding.newError(RESOURCE.mustNotNullInElse());
}
return false;
}
return true;
}
@Override public RelDataType inferReturnType(
SqlOperatorBinding opBinding) {
// REVIEW jvs 4-June-2005: can't these be unified?
if (!(opBinding instanceof SqlCallBinding)) {
return inferTypeFromOperands(opBinding);
}
return inferTypeFromValidator((SqlCallBinding) opBinding);
}
private static RelDataType inferTypeFromValidator(
SqlCallBinding callBinding) {
SqlCase caseCall = (SqlCase) callBinding.getCall();
SqlNodeList thenList = caseCall.getThenOperands();
ArrayList nullList = new ArrayList<>();
List argTypes = new ArrayList<>();
final SqlNodeList whenOperands = caseCall.getWhenOperands();
final RelDataTypeFactory typeFactory = callBinding.getTypeFactory();
for (int i = 0; i < thenList.size(); i++) {
SqlNode node = thenList.get(i);
RelDataType type = SqlTypeUtil.deriveType(callBinding, node);
SqlNode operand = whenOperands.get(i);
if (operand.getKind() == SqlKind.IS_NOT_NULL && type.isNullable()) {
SqlBasicCall call = (SqlBasicCall) operand;
if (call.getOperandList().get(0).equalsDeep(node, Litmus.IGNORE)) {
// We're sure that the type is not nullable if the kind is IS NOT NULL.
type = typeFactory.createTypeWithNullability(type, false);
}
}
argTypes.add(type);
if (SqlUtil.isNullLiteral(node, false)) {
nullList.add(node);
}
}
SqlNode elseOp = requireNonNull(caseCall.getElseOperand(),
() -> "elseOperand for " + caseCall);
argTypes.add(
SqlTypeUtil.deriveType(callBinding, elseOp));
if (SqlUtil.isNullLiteral(elseOp, false)) {
nullList.add(elseOp);
}
RelDataType ret = typeFactory.leastRestrictive(argTypes);
if (null == ret) {
boolean coerced = false;
if (callBinding.isTypeCoercionEnabled()) {
TypeCoercion typeCoercion = callBinding.getValidator().getTypeCoercion();
RelDataType commonType = typeCoercion.getWiderTypeFor(argTypes, true);
// commonType is always with nullability as false, we do not consider the
// nullability when deducing the common type. Use the deduced type
// (with the correct nullability) in SqlValidator
// instead of the commonType as the return type.
if (null != commonType) {
coerced = typeCoercion.caseWhenCoercion(callBinding);
if (coerced) {
ret = SqlTypeUtil.deriveType(callBinding);
}
}
}
if (!coerced) {
throw callBinding.newValidationError(RESOURCE.illegalMixingOfTypes());
}
}
final SqlValidatorImpl validator =
(SqlValidatorImpl) callBinding.getValidator();
requireNonNull(ret, () -> "return type for " + callBinding);
for (SqlNode node : nullList) {
validator.setValidatedNodeType(node, ret);
}
return ret;
}
private static RelDataType inferTypeFromOperands(SqlOperatorBinding opBinding) {
final RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
final List argTypes = opBinding.collectOperandTypes();
assert (argTypes.size() % 2) == 1 : "odd number of arguments expected: "
+ argTypes.size();
assert argTypes.size() > 1 : "CASE must have more than 1 argument. Given "
+ argTypes.size() + ", " + argTypes;
List thenTypes = new ArrayList<>();
for (int j = 1; j < (argTypes.size() - 1); j += 2) {
RelDataType argType = argTypes.get(j);
if (opBinding instanceof RexCallBinding) {
final RexCallBinding rexCallBinding = (RexCallBinding) opBinding;
final RexNode whenNode = rexCallBinding.operands().get(j - 1);
final RexNode thenNode = rexCallBinding.operands().get(j);
if (whenNode.getKind() == SqlKind.IS_NOT_NULL && argType.isNullable()) {
// Type is not nullable if the kind is IS NOT NULL.
final RexCall isNotNullCall = (RexCall) whenNode;
if (isNotNullCall.getOperands().get(0).equals(thenNode)) {
argType = typeFactory.createTypeWithNullability(argType, false);
}
}
}
thenTypes.add(argType);
}
thenTypes.add(Iterables.getLast(argTypes));
return requireNonNull(
typeFactory.leastRestrictive(thenTypes),
() -> "Can't find leastRestrictive type for " + thenTypes);
}
@Override public SqlOperandCountRange getOperandCountRange() {
return SqlOperandCountRanges.any();
}
@Override public SqlSyntax getSyntax() {
return SqlSyntax.SPECIAL;
}
@SuppressWarnings("argument.type.incompatible")
@Override public SqlCall createCall(
@Nullable SqlLiteral functionQualifier,
SqlParserPos pos,
@Nullable SqlNode... operands) {
assert functionQualifier == null;
assert operands.length == 4;
return new SqlCase(pos, operands[0], (SqlNodeList) operands[1],
(SqlNodeList) operands[2], operands[3]);
}
@Override public void unparse(SqlWriter writer, SqlCall call_, int leftPrec,
int rightPrec) {
SqlCase kase = (SqlCase) call_;
final SqlWriter.Frame frame =
writer.startList(SqlWriter.FrameTypeEnum.CASE, "CASE", "END");
assert kase.whenList.size() == kase.thenList.size();
if (kase.value != null) {
kase.value.unparse(writer, 0, 0);
}
for (Pair pair : Pair.zip(kase.whenList, kase.thenList)) {
writer.sep("WHEN");
pair.left.unparse(writer, 0, 0);
writer.sep("THEN");
pair.right.unparse(writer, 0, 0);
}
SqlNode elseExpr = kase.elseExpr;
if (elseExpr != null) {
writer.sep("ELSE");
elseExpr.unparse(writer, 0, 0);
}
writer.endList(frame);
}
}