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();

Reply via email to