-
Notifications
You must be signed in to change notification settings - Fork 2.5k
[CALCITE-7464] Refactor type coercion in array/map functions to avoid mutating operands as side effects #4869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. might be a breaking change for downstream projects who do not use |
||
| return true; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -200,6 +200,15 @@ boolean builtinFunctionCoercion( | |
| List<RelDataType> operandTypes, | ||
| List<SqlTypeFamily> expectedFamilies); | ||
|
|
||
| /** | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
||
There was a problem hiding this comment.
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.adjustTypeForArrayConstructoris not considered in this PR?