This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 1110bb11cf [CALCITE-6743] Type inference for ARRAY_INSERT function
produces an inconsistent result
1110bb11cf is described below
commit 1110bb11cfefea45d126607c246a69882bd5d366
Author: Mihai Budiu <[email protected]>
AuthorDate: Sat Aug 8 17:04:54 2026 -0700
[CALCITE-6743] Type inference for ARRAY_INSERT function produces an
inconsistent result
Signed-off-by: Mihai Budiu <[email protected]>
---
.../calcite/sql/fun/SqlLibraryOperators.java | 9 +--
.../calcite/sql/validate/SqlValidatorUtil.java | 20 ++++++-
.../org/apache/calcite/test/SqlValidatorTest.java | 64 ++++++++++++++++++++++
3 files changed, 86 insertions(+), 7 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 6b20e97997..6e328261c6 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
@@ -1593,12 +1593,13 @@ 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
+ // make explicit CAST for array elements and inserted element to the
biggest type.
+ // For array_insert, 0 is the array arg and 2 is the inserted element
+ if (!elementType2.equalsSansFieldNamesAndNullability(type)) {
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 2);
+ }
+ if (!componentType.equalsSansFieldNamesAndNullability(type)) {
SqlValidatorUtil.
adjustTypeForArrayFunctions(type, opBinding, 0);
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
index 3b67e5bbc3..84495c0a66 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
@@ -1439,6 +1439,7 @@ public static void adjustTypeForArrayFunctions(
RelDataType targetType, SqlOperatorBinding opBinding, int... indexes) {
if (opBinding instanceof SqlCallBinding) {
requireNonNull(targetType, "array function target type");
+ final SqlValidator validator = ((SqlCallBinding)
opBinding).getValidator();
SqlCall call = ((SqlCallBinding) opBinding).getCall();
List<SqlNode> operands = call.getOperandList();
for (int idx : indexes) {
@@ -1448,9 +1449,19 @@ public static void adjustTypeForArrayFunctions(
// 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));
+ call.setOperand(idx, castArrayElementTo(validator, operand,
targetType));
+ // The rewrite changes the element types of the array constructor,
+ // so the type the validator has recorded for it must change too
+ RelDataType priorType =
validator.getValidatedNodeTypeIfKnown(operand);
+ if (priorType != null) {
+ validator.setValidatedNodeType(operand,
+ SqlTypeUtil.createArrayType(opBinding.getTypeFactory(),
+ targetType, priorType.isNullable()));
+ }
} else {
- call.setOperand(idx, castTo(operand, targetType));
+ SqlNode cast = castTo(operand, targetType);
+ call.setOperand(idx, cast);
+ validator.setValidatedNodeType(cast, targetType);
}
}
}
@@ -1536,11 +1547,13 @@ private static SqlNode castTo(SqlNode node, RelDataType
type) {
* Each element of original 'node' is cast to the desired 'type', preserving
the
* nullability of the 'type'.
*
+ * @param validator Validator used, to record the new types
* @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) {
+ private static SqlNode castArrayElementTo(SqlValidator validator,
+ SqlNode node, RelDataType type) {
int i = 0;
for (SqlNode operand : ((SqlBasicCall) node).getOperandList()) {
SqlNode castedOperand =
@@ -1548,6 +1561,7 @@ private static SqlNode castArrayElementTo(SqlNode node,
RelDataType type) {
operand,
SqlTypeUtil.convertTypeToSpec(type).withNullable(type.isNullable()));
((SqlBasicCall) node).setOperand(i++, castedOperand);
+ validator.setValidatedNodeType(castedOperand, type);
}
return node;
}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 6c10478dbd..892d68f117 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -73,6 +73,7 @@
import org.apache.calcite.testlib.annotations.LocaleEnUs;
import org.apache.calcite.util.Bug;
import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.calcite.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -107,6 +108,7 @@
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasToString;
@@ -14433,6 +14435,68 @@ private void checkCustomColumnResolving(String table) {
assertThat(cast.getParserPosition().getLineNum(), is(1));
}
+ /** Test case for <a
href="https://issues.apache.org/jira/browse/CALCITE-6743">
+ * [CALCITE-6743] Type inference for ARRAY_INSERT function produces an
+ * inconsistent result</a>. */
+ @Test void testArrayFunctionAdjustedOperandTypes() throws SqlParseException {
+ // The array constructor is rewritten to ARRAY(CAST(1 AS DOUBLE), ...);
+ // its registered type must be DOUBLE ARRAY, not INTEGER ARRAY
+ final SqlCall call =
+ checkArrayOperandType("select array_insert(array(1, 2, 3), 3, cast(4
as double))",
+ "DOUBLE ARRAY NOT NULL", "DOUBLE", "DOUBLE NOT NULL");
+ // The inserted element already has the target type; it must keep the
+ // user's cast, without a redundant second cast around it
+ assertThat(Util.last(call.getOperandList()),
+ hasToString("CAST(4 AS DOUBLE)"));
+ checkArrayOperandType(
+ "select array_append(array(1, 2, 3), cast(4 as double))",
+ "DOUBLE NOT NULL ARRAY NOT NULL", "DOUBLE NOT NULL",
+ "DOUBLE NOT NULL");
+ checkArrayOperandType(
+ "select array_prepend(array(1, 2, 3), cast(4 as double))",
+ "DOUBLE NOT NULL ARRAY NOT NULL", "DOUBLE NOT NULL",
+ "DOUBLE NOT NULL");
+ // Only the inserted element is cast (to the array component type);
+ // the array constructor keeps its original type
+ checkArrayOperandType(
+ "select array_append(array(1, 2, 3), cast(4 as tinyint))",
+ "INTEGER NOT NULL ARRAY NOT NULL", "INTEGER NOT NULL",
+ "INTEGER NOT NULL");
+ }
+
+ /** Validates a query whose select list is a single call to an array
+ * function with an array constructor as its first argument; checks the
+ * validated types of the array argument, of the elements of the
+ * constructor, and of the inserted element after the function's type
+ * inference has adjusted them. Returns the validated call. */
+ private SqlCall checkArrayOperandType(String sql, String expectedArrayType,
+ String expectedElementType, String expectedInsertedType)
+ throws SqlParseException {
+ final SqlParser parser = SqlParser.create(sql, SqlParser.config());
+ final SqlNode node = parser.parseQuery();
+ final SqlValidator validator = fixture()
+ .withOperatorTable(operatorTableFor(SqlLibrary.SPARK))
+ .factory.createValidator();
+ final SqlSelect select = (SqlSelect) validator.validate(node);
+ final SqlCall call = (SqlCall) select.getSelectList().get(0);
+ final SqlNode array = call.getOperandList().get(0);
+ final RelDataType arrayType = validator.getValidatedNodeType(array);
+ assertThat(arrayType.getFullTypeString(), is(expectedArrayType));
+ final RelDataType componentType = arrayType.getComponentType();
+ assertThat(componentType, notNullValue());
+ for (SqlNode element : ((SqlCall) array).getOperandList()) {
+ final RelDataType elementType = validator.getValidatedNodeType(element);
+ assertThat(elementType.getFullTypeString(), is(expectedElementType));
+ assertThat(elementType, is(componentType));
+ }
+ // The last operand is the inserted element; its type may differ from
+ // the array component type only in nullability
+ final SqlNode inserted = Util.last(call.getOperandList());
+ assertThat(validator.getValidatedNodeType(inserted).getFullTypeString(),
+ is(expectedInsertedType));
+ return call;
+ }
+
@Test void testValidateParameterizedExpression() throws SqlParseException {
final SqlParser.Config config = SqlParser.config();
final SqlValidator validator = fixture().factory.createValidator();