From 8b86e86d2fc9e828be2e3b2ad891e0b2b88afe30 Mon Sep 17 00:00:00 2001 From: Ran Tao Date: Thu, 14 Dec 2023 11:46:03 +0800 Subject: [PATCH] Handle array operandTypeChecker bug --- .../calcite/sql/fun/SqlLibraryOperators.java | 2 +- .../apache/calcite/sql/type/OperandTypes.java | 62 +++++++++++++++++++ .../apache/calcite/test/SqlOperatorTest.java | 13 ++-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index a81f93910b0..427e305d503 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -1119,7 +1119,7 @@ private static RelDataType arrayReturnType(SqlOperatorBinding opBinding) { public static final SqlFunction ARRAY = SqlBasicFunction.create("ARRAY", SqlLibraryOperators::arrayReturnType, - OperandTypes.SAME_VARIADIC, + OperandTypes.ARRAY_FUNCTION, SqlFunctionCategory.SYSTEM); private static RelDataType mapReturnType(SqlOperatorBinding opBinding) { diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java index 295c95e0f33..6d4228d1f88 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java +++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java @@ -26,6 +26,7 @@ import org.apache.calcite.sql.SqlNode; import org.apache.calcite.sql.SqlOperandCountRange; import org.apache.calcite.sql.SqlOperator; +import org.apache.calcite.sql.SqlOperatorBinding; import org.apache.calcite.sql.SqlUtil; import org.apache.calcite.sql.validate.SqlValidatorScope; import org.apache.calcite.util.ImmutableIntList; @@ -560,6 +561,9 @@ public static SqlOperandTypeChecker variadic( public static final SqlSingleOperandTypeChecker MAP = family(SqlTypeFamily.MAP); + public static final SqlOperandTypeChecker ARRAY_FUNCTION = + new ArrayFunctionOperandTypeChecker(); + public static final SqlOperandTypeChecker ARRAY_ELEMENT = new ArrayElementOperandTypeChecker(); @@ -1225,6 +1229,64 @@ private static class MapFromEntriesOperandTypeChecker } } + /** + * Operand type-checking strategy for a ARRAY function, it allows empty array. + * + *

The reason it overrides SameOperandTypeChecker#checkOperandTypesImpl is that it needs + * to handle the scenario where row/struct type and NULL exist simultaneously in array. + * This scenario need be supported, but will be rejected by the current checkOperandTypesImpl. + */ + private static class ArrayFunctionOperandTypeChecker + extends SameOperandTypeChecker { + + ArrayFunctionOperandTypeChecker() { + // The args of array are non-fixed, so we set to -1 here. then operandCount + // can dynamically set according to the number of input args. + // details please see SameOperandTypeChecker#getOperandList. + super(-1); + } + + @Override protected boolean checkOperandTypesImpl( + SqlOperatorBinding operatorBinding, + boolean throwOnFailure, + @Nullable SqlCallBinding callBinding) { + if (throwOnFailure && callBinding == null) { + throw new IllegalArgumentException( + "callBinding must be non-null in case throwOnFailure=true"); + } + int nOperandsActual = nOperands; + if (nOperandsActual == -1) { + nOperandsActual = operatorBinding.getOperandCount(); + } + RelDataType[] types = new RelDataType[nOperandsActual]; + final List operandList = + getOperandList(operatorBinding.getOperandCount()); + for (int i : operandList) { + types[i] = operatorBinding.getOperandType(i); + } + int prev = -1; + for (int i : operandList) { + if (prev >= 0) { + // we replace SqlTypeUtil.isComparable with SqlTypeUtil.leastRestrictiveForComparison + // to handle struct type and NULL constant. + // details please see: https://issues.apache.org/jira/browse/CALCITE-6163 + RelDataType type = + SqlTypeUtil.leastRestrictiveForComparison(operatorBinding.getTypeFactory(), + types[i], types[prev]); + if (type == null) { + if (!throwOnFailure) { + return false; + } + throw requireNonNull(callBinding, "callBinding").newValidationError( + RESOURCE.needSameTypeParameter()); + } + } + prev = i; + } + return true; + } + } + /** * Operand type-checking strategy for a MAP function, it allows empty map. */ diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 1d15d3cfa9d..2f21b83dbd8 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -10546,10 +10546,10 @@ private static void checkArrayConcatAggFuncFails(SqlOperatorFixture t) { "RecordType(NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT NULL ARRAY NOT NULL"); f2.checkScalar("array(row(1, 2))", "[{1, 2}]", "RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT NULL ARRAY NOT NULL"); - f2.checkFails("^array(row(1, 2), null)^", - "Parameters must be of the same type", false); - f2.checkFails("^array(null, row(1, 2))^", - "Parameters must be of the same type", false); + f2.checkScalar("array(row(1, 2), null)", + "[{1, 2}, null]", "RecordType(INTEGER EXPR$0, INTEGER EXPR$1) ARRAY NOT NULL"); + f2.checkScalar("array(null, row(1, 2))", + "[null, {1, 2}]", "RecordType(INTEGER EXPR$0, INTEGER EXPR$1) ARRAY NOT NULL"); f2.checkScalar("array(row(1, null), row(2, null))", "[{1, null}, {2, null}]", "RecordType(INTEGER NOT NULL EXPR$0, NULL EXPR$1) NOT NULL ARRAY NOT NULL"); f2.checkScalar("array(row(null, 1), row(null, 2))", "[{null, 1}, {null, 2}]", @@ -10560,6 +10560,11 @@ private static void checkArrayConcatAggFuncFails(SqlOperatorFixture t) { "RecordType(INTEGER EXPR$0, INTEGER EXPR$1) NOT NULL ARRAY NOT NULL"); f2.checkScalar("array(row(1, 2), row(3, 4))", "[{1, 2}, {3, 4}]", "RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT NULL ARRAY NOT NULL"); + // checkFails + f2.checkFails("^array(row(1), row(2, 3))^", + "Parameters must be of the same type", false); + f2.checkFails("^array(row(1), row(2, 3), null)^", + "Parameters must be of the same type", false); // calcite default cast char type will fill extra spaces f2.checkScalar("array(1, 2, 'Hi')", "[1 , 2 , Hi]", "CHAR(2) NOT NULL ARRAY NOT NULL");