This is an automated email from the ASF dual-hosted git repository.

xiong 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 0dd30b5cd7 [CALCITE-6950] Use ANY operator to check if an element 
exists in an array throws exception
0dd30b5cd7 is described below

commit 0dd30b5cd72353d84825d194b3739766db8e921c
Author: Xiong Duan <[email protected]>
AuthorDate: Thu Jun 12 07:51:41 2025 +0800

    [CALCITE-6950] Use ANY operator to check if an element exists in an array 
throws exception
---
 .../calcite/sql/fun/SqlQuantifyOperator.java       | 43 ++++++++++---
 .../sql/validate/implicit/TypeCoercion.java        | 18 ++++++
 .../sql/validate/implicit/TypeCoercionImpl.java    | 70 ++++++++++++++++++++++
 .../apache/calcite/test/SqlToRelConverterTest.java | 29 +++++++++
 .../apache/calcite/test/SqlToRelConverterTest.xml  | 44 ++++++++++++++
 .../org/apache/calcite/test/SqlOperatorTest.java   | 46 ++++++++++++++
 6 files changed, 243 insertions(+), 7 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java
index dbd02f8e86..aeb770acba 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlQuantifyOperator.java
@@ -17,16 +17,21 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlBasicCall;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 
+import com.google.common.collect.ImmutableList;
+
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.util.List;
@@ -102,15 +107,33 @@ public class SqlQuantifyOperator extends SqlInOperator {
     final SqlNode left = call.operand(0);
     final SqlNode right = call.operand(1);
     if (right instanceof SqlNodeList && ((SqlNodeList) right).size() == 1) {
-      final RelDataType rightType =
+      RelDataType rightType =
           validator.deriveType(scope, ((SqlNodeList) right).get(0));
       if (SqlTypeUtil.isCollection(rightType)) {
-        final RelDataType componentRightType =
-            requireNonNull(rightType.getComponentType());
-        final RelDataType leftType = validator.deriveType(scope, left);
-        if (SqlTypeUtil.sameNamedType(componentRightType, leftType)
-            || SqlTypeUtil.isNull(leftType)
-            || SqlTypeUtil.isNull(componentRightType)) {
+        RelDataType componentRightType = 
requireNonNull(rightType.getComponentType());
+        RelDataType leftType = validator.deriveType(scope, left);
+        boolean isCompatibleType = isCompatibleType(componentRightType, 
leftType);
+        SqlCall sqlCall =
+            new SqlBasicCall(call.getOperator(),
+                ImmutableList.of(left, ((SqlNodeList) right).get(0)), 
SqlParserPos.ZERO);
+        SqlCallBinding callBinding = new SqlCallBinding(validator, scope, 
sqlCall);
+        if (!isCompatibleType && callBinding.isTypeCoercionEnabled()) {
+          boolean coerced = callBinding.getValidator().getTypeCoercion()
+              .quantifyOperationCoercion(callBinding);
+          if (coerced) {
+            // Update the node data type if we coerced any type.
+            call.setOperand(0,
+                callBinding.operand(0));
+            call.setOperand(1,
+                new SqlNodeList(ImmutableList.of(callBinding.operand(1)), 
SqlParserPos.ZERO));
+            leftType = validator.deriveType(scope, call.operand(0));
+            rightType = validator.deriveType(scope, ((SqlNodeList) 
right).get(0));
+            componentRightType = rightType.getComponentType();
+            requireNonNull(componentRightType, "componentRightType");
+            isCompatibleType = isCompatibleType(componentRightType, leftType);
+          }
+        }
+        if (isCompatibleType) {
           return validator.getTypeFactory().createTypeWithNullability(
               validator.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN),
               rightType.isNullable()
@@ -124,6 +147,12 @@ public class SqlQuantifyOperator extends SqlInOperator {
     return null;
   }
 
+  private static boolean isCompatibleType(RelDataType leftType, RelDataType 
rightType) {
+    return SqlTypeUtil.sameNamedType(rightType, leftType)
+        || SqlTypeUtil.isNull(leftType)
+        || SqlTypeUtil.isNull(rightType);
+  }
+
   @Override public SqlOperator not() {
     switch (kind) {
     case SOME:
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercion.java 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercion.java
index 525dfee2b2..f3d1d528fd 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercion.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercion.java
@@ -120,6 +120,24 @@ boolean rowTypeCoercion(
    */
   boolean inOperationCoercion(SqlCallBinding binding);
 
+  /** Handles type coercion for Quantify operations {@link 
org.apache.calcite.sql.fun.SqlQuantifyOperator}.
+   *
+   * <p>For example:
+   * <ul>
+   * <li>{@code 1.0 = some (ARRAY[2,3,null])}
+   * <li>{@code 'timestamp 1970-01-01 01:23:47' =
+   * any (array['1970-01-01 01:23:45', '1970-01-01 01:23:46'])}
+   * <li>{@code WITH tb as
+   * (select
+   * array(SELECT * FROM (VALUES ('1970-01-01 01:23:45'), ('1970-01-01 
01:23:46'))
+   * as x(a)) as a)
+   * SELECT timestamp '1970-01-01 01:23:45' >= some (a) FROM tb}
+   * </ul>
+   *
+   * <p>See {@link TypeCoercionImpl} for default strategies.
+   * */
+  boolean quantifyOperationCoercion(SqlCallBinding binding);
+
   /** Coerces operand of binary arithmetic expressions to Numeric type.*/
   boolean binaryArithmeticCoercion(SqlCallBinding binding);
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java
index 10e1d36d0c..f144ec8399 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercionImpl.java
@@ -598,6 +598,76 @@ private boolean coalesceCoercion(SqlCallBinding 
callBinding) {
     return false;
   }
 
+  /**
+   * {@inheritDoc}
+   *
+   * <p>STRATEGIES
+   *
+   * <p>To determine the common type:
+   *
+   * <ul>
+   *
+   * <li>When the LHS has a Simple type and RHS has a Collection type 
determined by {@link SqlTypeUtil#isCollection},
+   * to find the common type of LHS's type and RHS's component type.
+   * <li>If the common type differs from the LHS type, then coerced LHS type,
+   * and the nullability of the result type remains unchanged.
+   * <li>Create a new Collection type that matches the common type,
+   * the nullability of the new type keep same as RHS's component type and 
RHS's collection type.
+   * <li>If this new type differs from the RHS type, adjust the RHS type as 
needed.
+   *
+   *<pre>
+   * field1       ARRAY(field2, field3, field4)
+   *    |                |       |       |
+   *    |                +-------+-------+
+   *    |                        |
+   *    |                  component type
+   *    |                        |
+   *    +------common type-------+
+   *</pre>
+   *
+   * <li>If LHS type and the component type of RHS are different from the 
common type,
+   * CAST needs to be added.
+   * </ul>
+   */
+  @Override public boolean quantifyOperationCoercion(SqlCallBinding binding) {
+    final RelDataType type1 = binding.getOperandType(0);
+    final RelDataType collectionType = binding.getOperandType(1);
+    final RelDataType type2 = collectionType.getComponentType();
+    requireNonNull(type2, "type2");
+    final SqlCall sqlCall = binding.getCall();
+    final SqlValidatorScope scope = binding.getScope();
+    final SqlNode node1 = binding.operand(0);
+    final SqlNode node2 = binding.operand(1);
+    RelDataType widenType = commonTypeForBinaryComparison(type1, type2);
+    if (widenType == null) {
+      widenType = getTightestCommonType(type1, type2);
+    }
+    if (widenType == null) {
+      return false;
+    }
+    final RelDataType leftWidenType =
+        binding.getTypeFactory().enforceTypeWithNullability(widenType, 
type1.isNullable());
+    boolean coercedLeft =
+        coerceOperandType(scope, sqlCall, 0, leftWidenType);
+    if (coercedLeft) {
+      updateInferredType(node1, leftWidenType);
+    }
+    final RelDataType rightWidenType =
+        binding.getTypeFactory().enforceTypeWithNullability(widenType, 
type2.isNullable());
+    RelDataType collectionWidenType =
+        binding.getTypeFactory().createArrayType(rightWidenType, -1);
+    collectionWidenType =
+        binding
+            .getTypeFactory()
+            .enforceTypeWithNullability(collectionWidenType, 
collectionType.isNullable());
+    boolean coercedRight =
+        coerceOperandType(scope, sqlCall, 1, collectionWidenType);
+    if (coercedRight) {
+      updateInferredType(node2, collectionWidenType);
+    }
+    return coercedLeft || coercedRight;
+  }
+
   @Override public boolean builtinFunctionCoercion(
       SqlCallBinding binding,
       List<RelDataType> operandTypes,
diff --git 
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index cc6a80488e..96d956a040 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -3789,6 +3789,35 @@ void checkCorrelatedMapSubQuery(boolean expand) {
         .ok();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6950";>[CALCITE-6950]
+   * Use ANY operator to check if an element exists in an array throws 
exception</a>. */
+  @Test void testQuantifyOperatorsWithTypeCoercion() {
+    sql("SELECT 1.0 = some (ARRAY[2,null,3])")
+        .withExpand(false)
+        .ok();
+  }
+
+  @Test void testQuantifyOperatorsWithTypeCoercion2() {
+    sql("SELECT 3 = some (ARRAY[1.0, 2.0])")
+        .withExpand(false)
+        .ok();
+  }
+
+  @Test void testQuantifyOperatorsWithTypeCoercion3() {
+    sql("SELECT '1970-01-01 01:23:45' = any (array[timestamp '1970-01-01 
01:23:45',"
+        + "timestamp '1970-01-01 01:23:46'])")
+        .withExpand(false)
+        .ok();
+  }
+
+  @Test void testQuantifyOperatorsWithTypeCoercion4() {
+    sql("SELECT timestamp '1970-01-01 01:23:45' = any (array['1970-01-01 
01:23:45',"
+        + "'1970-01-01 01:23:46'])")
+        .withExpand(false)
+        .ok();
+  }
+
   @Test void testQualifyInCorrelatedSubQuery() {
     // The QUALIFY clause is inside a WHERE EXISTS and references columns from
     // the enclosing query.
diff --git 
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml 
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index 0c7463f7b9..760d4c5465 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -6727,6 +6727,50 @@ LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$2])
     LogicalProject(EMPNO=[$0], ENAME=[$1], DEPTNO=[$7], 
QualifyExpression=[=(ROW_NUMBER() OVER (PARTITION BY $1 ORDER BY $7), 1)])
       LogicalFilter(condition=[>($7, 5)])
         LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testQuantifyOperatorsWithTypeCoercion">
+    <Resource name="sql">
+      <![CDATA[SELECT 1.0 = some (ARRAY[2,null,3])]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EXPR$0=[= SOME(1.0:DECIMAL(11, 1), CAST(ARRAY(2, null:INTEGER, 
3)):DECIMAL(11, 1) ARRAY NOT NULL)])
+  LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testQuantifyOperatorsWithTypeCoercion2">
+    <Resource name="sql">
+      <![CDATA[SELECT 3 = some (ARRAY[1.0, 2.0])]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EXPR$0=[= SOME(3.0:DECIMAL(11, 1), CAST(ARRAY(1.0:DECIMAL(2, 
1), 2.0:DECIMAL(2, 1))):DECIMAL(11, 1) NOT NULL ARRAY NOT NULL)])
+  LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testQuantifyOperatorsWithTypeCoercion3">
+    <Resource name="sql">
+      <![CDATA[SELECT '1970-01-01 01:23:45' = any (array[timestamp '1970-01-01 
01:23:45',timestamp '1970-01-01 01:23:46'])]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EXPR$0=[= SOME(1970-01-01 01:23:45, ARRAY(1970-01-01 01:23:45, 
1970-01-01 01:23:46))])
+  LogicalValues(tuples=[[{ 0 }]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testQuantifyOperatorsWithTypeCoercion4">
+    <Resource name="sql">
+      <![CDATA[SELECT timestamp '1970-01-01 01:23:45' = any (array['1970-01-01 
01:23:45','1970-01-01 01:23:46'])]]>
+    </Resource>
+    <Resource name="plan">
+      <![CDATA[
+LogicalProject(EXPR$0=[= SOME(1970-01-01 01:23:45, CAST(ARRAY('1970-01-01 
01:23:45', '1970-01-01 01:23:46')):TIMESTAMP(0) NOT NULL ARRAY NOT NULL)])
+  LogicalValues(tuples=[[{ 0 }]])
 ]]>
     </Resource>
   </TestCase>
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 9313823fdc..a20ad2d326 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -16145,6 +16145,52 @@ void testTimestampDiff(boolean coercionEnabled) {
         "Values passed to = SOME operator must have compatible types", false);
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6950";>[CALCITE-6950]
+   * Use ANY operator to check if an element exists in an array throws 
exception</a>. */
+  @Test void testQuantifyOperatorsWithTypeCoercion() {
+    final SqlOperatorFixture f = fixture();
+    QUANTIFY_OPERATORS.forEach(operator -> f.setFor(operator, 
SqlOperatorFixture.VmName.EXPAND));
+
+    f.checkNull("1.0 = some (ARRAY[2,3,null])");
+    f.checkNull("1.0 = some (ARRAY[2,null,3])");
+    f.enableTypeCoercion(false).checkFails(
+        "^1.0 = some (ARRAY[2,3,null])^",
+        "Values passed to = SOME operator must have compatible types",
+        false);
+
+    f.checkBoolean("1.0 = some (ARRAY[1,2,null])", true);
+    f.checkBoolean("3.0 = some (ARRAY[1,2])", false);
+    f.enableTypeCoercion(false).checkFails(
+        "^3.0 = some (ARRAY[1,2])^",
+        "Values passed to = SOME operator must have compatible types",
+        false);
+
+    f.checkBoolean(
+        "'1970-01-01 01:23:45' = any (array[timestamp '1970-01-01 01:23:45',"
+            + "timestamp '1970-01-01 01:23:46'])", true);
+    f.checkBoolean(
+        "'1970-01-01 01:23:47' = any (array[timestamp '1970-01-01 01:23:45',"
+            + "timestamp '1970-01-01 01:23:46'])", false);
+    f.enableTypeCoercion(false).checkFails(
+        "^'1970-01-01 01:23:47' = any (array[timestamp '1970-01-01 01:23:45',"
+            + "timestamp '1970-01-01 01:23:46'])^",
+        "Values passed to = SOME operator must have compatible types",
+        false);
+
+    f.checkBoolean(
+        "cast('1970-01-01 01:23:45' as timestamp) = any (array['1970-01-01 
01:23:45',"
+            + "'1970-01-01 01:23:46'])", true);
+    f.checkBoolean(
+        "cast('1970-01-01 01:23:47' as timestamp) = any (array['1970-01-01 
01:23:45',"
+            + "'1970-01-01 01:23:46'])", false);
+    f.enableTypeCoercion(false).checkFails(
+        "^cast('1970-01-01 01:23:47' as timestamp) = any (array['1970-01-01 
01:23:45',"
+            + "'1970-01-01 01:23:46'])^",
+        "Values passed to = SOME operator must have compatible types",
+        false);
+  }
+
   @Test void testAnyValueFunc() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.ANY_VALUE, VM_EXPAND);

Reply via email to