Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1460,20 +1460,6 @@ private static RelDataType arrayAppendPrependReturnType(SqlOperatorBinding opBin
type = opBinding.getTypeFactory().createTypeWithNullability(type, true);
}

// make explicit CAST for array elements and inserted element to the biggest type
// if array component type not equals to inserted element type
if (!componentType.equalsSansFieldNames(elementType)) {
// 0, 1 is the operand index to be CAST
// For array_append/array_prepend, 0 is the array arg and 1 is the inserted element
if (componentType.equalsSansFieldNames(type)) {
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 1);
} else {
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 0);
}
}

return SqlTypeUtil.createArrayType(opBinding.getTypeFactory(), type, arrayType.isNullable());
}

Expand Down Expand Up @@ -1581,9 +1567,6 @@ private static RelDataType arrayInsertReturnType(SqlOperatorBinding opBinding) {
final RelDataType elementType2 = operandTypes.get(2);
requireNonNull(componentType, () -> "componentType of " + arrayType);

// we don't need to do leastRestrictive on componentType and elementType,
// because in operand checker we limit the elementType such that it equals the array component
// type. So we use componentType directly.
RelDataType type =
opBinding.getTypeFactory().leastRestrictive(
ImmutableList.of(componentType, elementType2));
Expand All @@ -1592,15 +1575,6 @@ private static RelDataType arrayInsertReturnType(SqlOperatorBinding opBinding) {
// The spec says that "ARRAY_INSERT may pad the array with NULL values if the
// position is large", it implies that in the result the element type is always nullable.
type = opBinding.getTypeFactory().createTypeWithNullability(type, true);
// make explicit CAST for array elements and inserted element to the biggest type
// if array component type is not equal to the inserted element type
if (!componentType.equalsSansFieldNamesAndNullability(elementType2)) {
// For array_insert, 0 is the array arg and 2 is the inserted element
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 2);
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 0);
}
boolean nullable = arrayType.isNullable() || elementType1.isNullable();
return SqlTypeUtil.createArrayType(opBinding.getTypeFactory(), type, nullable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;

Expand Down Expand Up @@ -52,9 +51,6 @@ public SqlMapValueConstructor() {
getComponentTypes(
opBinding.getTypeFactory(), opBinding.collectOperandTypes());

// explicit cast elements to component type if they are not same
SqlValidatorUtil.adjustTypeForMapConstructor(type, opBinding);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason SqlValidatorUtil.adjustTypeForArrayConstructor is not considered in this PR?


return SqlTypeUtil.createMapType(
opBinding.getTypeFactory(),
requireNonNull(type.left, "inferred key type"),
Expand All @@ -81,6 +77,10 @@ public SqlMapValueConstructor() {
}
return false;
}
if (callBinding.isTypeCoercionEnabled()) {
callBinding.getValidator().getTypeCoercion()
.collectionFunctionCoercion(callBinding);
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ public ArrayElementOperandTypeChecker(boolean arrayMayBeNull, boolean elementMay

return false;
}
if (callBinding.isTypeCoercionEnabled()) {
callBinding.getValidator().getTypeCoercion()
.collectionFunctionCoercion(callBinding);
}
Comment on lines +98 to +101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be a breaking change for downstream projects who do not use typeCoercion
imho need at least put info about this in release notes

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ public ArrayInsertOperandTypeChecker() {
return false;
}

if (callBinding.isTypeCoercionEnabled()) {
callBinding.getValidator().getTypeCoercion()
.collectionFunctionCoercion(callBinding);
}
return true;
}
}
34 changes: 4 additions & 30 deletions core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.util.SqlBasicVisitor;
import org.apache.calcite.sql.validate.SqlLambdaScope;
import org.apache.calcite.sql.validate.SqlValidator;
Expand Down Expand Up @@ -1557,35 +1555,11 @@ private static class MapFunctionOperandTypeChecker
}
return false;
}
// Insert implicit casts for operands whose SqlTypeName differs
// from the inferred key/value type.
coerceOperands(callBinding, argTypes,
componentType.left, componentType.right);
return true;
}

/** Casts operands whose {@code SqlTypeName} differs from the
* target key or value type. Operands at even positions are keys,
* odd positions are values. */
private static void coerceOperands(SqlCallBinding callBinding,
List<RelDataType> operandTypes,
RelDataType keyType, RelDataType valueType) {
final SqlValidator validator = callBinding.getValidator();
final SqlCall call = callBinding.getCall();
final List<SqlNode> operands = call.getOperandList();
for (int i = 0; i < operands.size(); i++) {
final RelDataType targetType = i % 2 == 0 ? keyType : valueType;
if (operandTypes.get(i).getSqlTypeName()
!= targetType.getSqlTypeName()) {
final SqlNode castNode =
SqlStdOperatorTable.CAST.createCall(SqlParserPos.ZERO,
operands.get(i),
SqlTypeUtil.convertTypeToSpec(targetType)
.withNullable(targetType.isNullable()));
call.setOperand(i, castNode);
validator.setValidatedNodeType(castNode, targetType);
}
if (callBinding.isTypeCoercionEnabled()) {
callBinding.getValidator().getTypeCoercion()
.collectionFunctionCoercion(callBinding);
}
return true;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1406,68 +1406,6 @@ public static void adjustTypeForArrayConstructor(
}
}

/**
* Adjusts the types of specified operands in an array operation to match a given target type.
* This is particularly useful in the context of SQL operations involving array functions,
* where it's necessary to ensure that all operands have consistent types for the operation
* to be valid.
*
* <p>This method operates on the assumption that the operands to be adjusted are part of a
* {@link SqlCall}, which is bound within a {@link SqlOperatorBinding}. The operands to be
* cast are identified by their indexes within the {@code operands} list of the {@link SqlCall}.
* The method performs a dynamic check to determine if an operand is a basic call to an array.
* If so, it casts each element within the array to the target type.
* Otherwise, it casts the operand itself to the target type.
*
* <p>Example usage: For an operation like {@code array_append(array(1,2), cast(2 as tinyint))},
* if targetType is double, this method would ensure that the elements of the
* first array and the second operand are cast to double.
*
* @param targetType The target {@link RelDataType} to which the operands should be cast.
* @param opBinding The {@link SqlOperatorBinding} context, which provides access to the
* {@link SqlCall} and its operands.
* @param indexes The indexes of the operands within the {@link SqlCall} that need to be
* adjusted to the target type.
* @throws NullPointerException if {@code targetType} is {@code null}.
*/
public static void adjustTypeForArrayFunctions(
RelDataType targetType, SqlOperatorBinding opBinding, int... indexes) {
if (opBinding instanceof SqlCallBinding) {
requireNonNull(targetType, "array function target type");
SqlCall call = ((SqlCallBinding) opBinding).getCall();
List<SqlNode> operands = call.getOperandList();
for (int idx : indexes) {
SqlNode operand = operands.get(idx);
if (operand instanceof SqlBasicCall
// not use SqlKind to compare because some other array function forms
// such as spark array, the SqlKind is other function.
// however, the name is same for those different array forms.
&& "ARRAY".equals(((SqlBasicCall) operand).getOperator().getName())) {
call.setOperand(idx, castArrayElementTo(operand, targetType));
} else {
call.setOperand(idx, castTo(operand, targetType));
}
}
}
}

/**
* When the map key or value does not equal the map component key type or value type,
* make explicit casting.
*
* @param componentType derived map pair component type
* @param opBinding description of call
*/
public static void adjustTypeForMapConstructor(
Pair<RelDataType, RelDataType> componentType, SqlOperatorBinding opBinding) {
if (opBinding instanceof SqlCallBinding) {
requireNonNull(componentType.getKey(), "map key type");
requireNonNull(componentType.getValue(), "map value type");
adjustTypeForMultisetConstructor(
componentType.getKey(), componentType.getValue(), (SqlCallBinding) opBinding);
}
}

/**
* Adjusts the types for operands in a SqlCallBinding during the construction of a sql collection
* type such as Array or Map. This method iterates from the operands of a {@link SqlCall}
Expand Down Expand Up @@ -1523,30 +1461,6 @@ private static SqlNode castTo(SqlNode node, RelDataType type) {
SqlTypeUtil.convertTypeToSpec(type).withNullable(type.isNullable()));
}

/**
* Creates a CAST operation that cast each element of the given {@link SqlNode} to the
* specified type. The {@link SqlNode} representing an array and a {@link RelDataType}
* representing the target type. This method uses the {@link SqlStdOperatorTable#CAST}
* operator to create a new {@link SqlCall} node representing a CAST operation.
* Each element of original 'node' is cast to the desired 'type', preserving the
* nullability of the 'type'.
*
* @param node the {@link SqlNode} the sqlnode representing an array
* @param type the target {@link RelDataType} the target type
* @return a new {@link SqlNode} representing the CAST operation
*/
private static SqlNode castArrayElementTo(SqlNode node, RelDataType type) {
int i = 0;
for (SqlNode operand : ((SqlBasicCall) node).getOperandList()) {
SqlNode castedOperand =
SqlStdOperatorTable.CAST.createCall(SqlParserPos.ZERO,
operand,
SqlTypeUtil.convertTypeToSpec(type).withNullable(type.isNullable()));
((SqlBasicCall) node).setOperand(i++, castedOperand);
}
return node;
}

//~ Inner Classes ----------------------------------------------------------

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,15 @@ boolean builtinFunctionCoercion(
List<RelDataType> operandTypes,
List<SqlTypeFamily> expectedFamilies);

/**
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type coercion can depend on functions, it's the other way around: functions can use some services from type coercion.

So I don't understand what this function means.

* Coerces operands of collection-related functions and constructors
* ({@code ARRAY_APPEND}, {@code ARRAY_PREPEND}, {@code ARRAY_INSERT},
* {@code MAP} constructor) to the least restrictive common type.
*/
default boolean collectionFunctionCoercion(SqlCallBinding binding) {
return false;
}

/**
* Non built-in functions (UDFs) type coercion, compare the types of arguments
* with rules:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Util;

import com.google.common.collect.ImmutableList;

import org.checkerframework.checker.nullness.qual.Nullable;

import java.math.BigDecimal;
Expand Down Expand Up @@ -688,6 +690,126 @@ private boolean coalesceCoercion(SqlCallBinding callBinding) {
return coercedLeft || coercedRight;
}

@Override public boolean collectionFunctionCoercion(SqlCallBinding binding) {
final SqlCall call = binding.getCall();
switch (call.getKind()) {
case MAP_VALUE_CONSTRUCTOR:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type coercion cannot depend on various functions like ARRAY_APPEND.

return mapCoercion(binding, false);
case ARRAY_APPEND:
case ARRAY_PREPEND:
return arrayElementCoercion(binding, 1);
case ARRAY_INSERT:
return arrayElementCoercion(binding, 2);
default:
if ("MAP".equals(call.getOperator().getName())) {
return mapCoercion(binding, true);
}
return false;
}
}

/**
* Coerces operands of a MAP constructor to the least restrictive key/value
* types. Keys are at even positions, values at odd positions.
*
* <p>The MAP value constructor ({@code MAP['k1', v1, ...]}) compares types
* via {@link RelDataType#equalsSansFieldNames}, so CHAR(5) vs CHAR(10)
* triggers a cast. The Spark-style {@code MAP()} function only compares
* {@link SqlTypeName}, so same-type-name differences are left alone.
*
* @param typeNameOnly if true, only coerce when SqlTypeName differs
*/
private boolean mapCoercion(SqlCallBinding binding, boolean typeNameOnly) {
final SqlCall call = binding.getCall();
final SqlValidatorScope scope = binding.getScope();
final List<RelDataType> operandTypes =
SqlTypeUtil.deriveType(binding, binding.operands());
if (operandTypes.isEmpty() || operandTypes.size() % 2 != 0) {
return false;
}
final RelDataType keyType =
factory.leastRestrictive(Util.quotientList(operandTypes, 2, 0));
final RelDataType valueType =
factory.leastRestrictive(Util.quotientList(operandTypes, 2, 1));
if (keyType == null || valueType == null) {
return false;
}
boolean coerced = false;
for (int i = 0; i < call.operandCount(); i++) {
final RelDataType targetType = i % 2 == 0 ? keyType : valueType;
final boolean needsCast = typeNameOnly
? operandTypes.get(i).getSqlTypeName() != targetType.getSqlTypeName()
: !operandTypes.get(i).equalsSansFieldNames(targetType);
if (needsCast) {
coerced = coerceOperandType(scope, call, i, targetType) || coerced;
}
}
return coerced;
}

/**
* Coerces the array operand (index 0) and the scalar element operand
* (at {@code elementIndex}) of ARRAY_APPEND, ARRAY_PREPEND or ARRAY_INSERT
* to the least restrictive common type.
*/
private boolean arrayElementCoercion(SqlCallBinding binding,
int elementIndex) {
final SqlCall call = binding.getCall();
final SqlValidatorScope scope = binding.getScope();
final RelDataType arrayType = binding.getOperandType(0);
final RelDataType componentType = arrayType.getComponentType();
if (componentType == null) {
return false;
}
final RelDataType elementType = binding.getOperandType(elementIndex);
final RelDataType targetType =
factory.leastRestrictive(
ImmutableList.of(componentType, elementType));
if (targetType == null) {
return false;
}
boolean coerced = false;
if (!SqlTypeUtil.equalSansNullability(factory, componentType, targetType)) {
coerced = coerceArrayOperand(scope, call, 0, targetType);
}
if (!SqlTypeUtil.equalSansNullability(factory, elementType, targetType)) {
coerced =
coerceOperandType(scope, call, elementIndex, targetType) || coerced;
}
return coerced;
}

/**
* Coerces an array operand's element type to {@code targetElementType}.
* If the operand is an array literal ({@code ARRAY[1, 2]}), each element
* is coerced individually; otherwise the operand is cast as a whole.
*/
private boolean coerceArrayOperand(
@Nullable SqlValidatorScope scope,
SqlCall call,
int index,
RelDataType targetElementType) {
final SqlNode operand = call.getOperandList().get(index);
if (operand instanceof SqlCall
&& (((SqlCall) operand).getKind() == SqlKind.ARRAY_VALUE_CONSTRUCTOR
|| "ARRAY".equals(((SqlCall) operand).getOperator().getName()))) {
final SqlCall arrayCall = (SqlCall) operand;
boolean coerced = false;
for (int i = 0; i < arrayCall.operandCount(); i++) {
coerced =
coerceOperandType(scope, arrayCall, i, targetElementType) || coerced;
}
return coerced;
}
final RelDataType operandType =
validator.deriveType(requireNonNull(scope, "scope"), operand);
final RelDataType targetArrayType =
factory.createTypeWithNullability(
factory.createArrayType(targetElementType, -1),
operandType.isNullable());
return coerceOperandType(scope, call, index, targetArrayType);
}

@Override public boolean builtinFunctionCoercion(
SqlCallBinding binding,
List<RelDataType> operandTypes,
Expand Down
Loading