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

mbudiu 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 e9f7dfc318 [CALCITE-6991] Validator cannot infer type for COALESCE 
when call is not expanded
e9f7dfc318 is described below

commit e9f7dfc318b15198df617a9cdb0f477d30997517
Author: Juntao Zhang <[email protected]>
AuthorDate: Wed Apr 30 18:33:34 2025 +0800

    [CALCITE-6991] Validator cannot infer type for COALESCE when call is not 
expanded
---
 .../apache/calcite/sql/fun/SqlCaseOperator.java    |  2 +-
 .../calcite/sql/fun/SqlCoalesceFunction.java       | 51 ++++++++++++++++++++++
 .../sql/validate/implicit/TypeCoercion.java        | 25 +++++++++--
 .../sql/validate/implicit/TypeCoercionImpl.java    | 39 ++++++++++++++++-
 .../org/apache/calcite/test/SqlValidatorTest.java  | 16 ++++++-
 5 files changed, 126 insertions(+), 7 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCaseOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCaseOperator.java
index 0c547fb876..4d5cbf9a6d 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCaseOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCaseOperator.java
@@ -272,7 +272,7 @@ private static RelDataType inferTypeFromValidator(
         // (with the correct nullability) in SqlValidator
         // instead of the commonType as the return type.
         if (null != commonType) {
-          coerced = typeCoercion.caseWhenCoercion(callBinding);
+          coerced = typeCoercion.caseOrEquivalentCoercion(callBinding);
           if (coerced) {
             ret = SqlTypeUtil.deriveType(callBinding);
           }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
index 1dcfe477f9..cd3c46040c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
@@ -16,21 +16,34 @@
  */
 package org.apache.calcite.sql.fun;
 
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
+import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlTypeTransforms;
+import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql.validate.implicit.TypeCoercion;
 import org.apache.calcite.util.Util;
 
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.ArrayList;
 import java.util.List;
 
+import static org.apache.calcite.util.Static.RESOURCE;
+
+import static java.util.Objects.requireNonNull;
+
 /**
  * The <code>COALESCE</code> function.
  */
@@ -80,4 +93,42 @@ public SqlCoalesceFunction() {
     assert call.getFunctionQuantifier() == null;
     return SqlCase.createSwitched(pos, null, whenList, thenList, elseExpr);
   }
+
+  @Override @NonNull public RelDataType inferReturnType(
+      SqlOperatorBinding opBinding) {
+    RelDataType returnType = 
getReturnTypeInference().inferReturnType(opBinding);
+    if (returnType == null
+        && opBinding instanceof SqlCallBinding
+        && ((SqlCallBinding) opBinding).isTypeCoercionEnabled()) {
+      SqlCallBinding callBinding = (SqlCallBinding) opBinding;
+      List<RelDataType> argTypes = new ArrayList<>();
+      for (SqlNode operand : callBinding.operands()) {
+        RelDataType type = SqlTypeUtil.deriveType(callBinding, operand);
+        argTypes.add(type);
+      }
+      TypeCoercion typeCoercion = callBinding.getValidator().getTypeCoercion();
+      RelDataType commonType = typeCoercion.getWiderTypeFor(argTypes, true);
+      if (null != commonType) {
+        // COALESCE type coercion, find a common type across all branches and 
casts
+        // operands to this common type if necessary.
+        boolean coerced = typeCoercion.caseOrEquivalentCoercion(callBinding);
+        if (coerced) {
+          return SqlTypeUtil.deriveType(callBinding);
+        }
+      }
+      throw callBinding.newValidationError(RESOURCE.illegalMixingOfTypes());
+    }
+
+    if (returnType == null) {
+      throw new IllegalArgumentException(
+          "Cannot infer return type for " + opBinding.getOperator() + "; 
operand types: "
+              + opBinding.collectOperandTypes());
+    }
+
+    return returnType;
+  }
+
+  @Override public SqlReturnTypeInference getReturnTypeInference() {
+    return requireNonNull(super.getReturnTypeInference(), 
"returnTypeInference");
+  }
 }
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 2a343cca1b..525dfee2b2 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
@@ -129,10 +129,29 @@ boolean rowTypeCoercion(
   /**
    * Coerces CASE WHEN statement branches to one common type.
    *
-   * <p>Rules: Find common type for all the then operands and else operands,
-   * then try to coerce the then/else operands to the type if needed.
+   * @deprecated Use {@link #caseOrEquivalentCoercion} instead.
    */
-  boolean caseWhenCoercion(SqlCallBinding binding);
+  @Deprecated boolean caseWhenCoercion(SqlCallBinding binding);
+
+  /**
+   * Type coercion in CASE WHEN, COALESCE, and NULLIF.
+   *
+   * <p>Rules:
+   * <ol>
+   *   <li>
+   *     CASE WHEN collect all the branches types including then
+   *     operands and else operands to find a common type, then cast the 
operands to the common type
+   *     when needed.</li>
+   *   <li>
+   *     COALESCE collect all the branches types to find a common type,
+   *     then cast the operands to the common type when needed.</li>
+   *   <li>
+   *     NULLIF returns the first operand if the two operands are not equal,
+   *     otherwise it returns a null value of the type of the first operand,
+   *     without return type coercion.</li>
+   * </ol>
+   */
+  boolean caseOrEquivalentCoercion(SqlCallBinding binding);
 
   /**
    * Type coercion with inferred type from passed in arguments and the
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 711c8d22dc..10e1d36d0c 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
@@ -383,11 +383,12 @@ protected boolean booleanEquality(SqlCallBinding binding,
   }
 
   /**
-   * CASE and COALESCE type coercion, collect all the branches types including 
then
+   * CASE WHEN type coercion, collect all the branches types including then
    * operands and else operands to find a common type, then cast the operands 
to the common type
    * when needed.
    */
-  @Override public boolean caseWhenCoercion(SqlCallBinding callBinding) {
+  @SuppressWarnings("deprecation")
+  public boolean caseWhenCoercion(SqlCallBinding callBinding) {
     // For sql statement like:
     // `case when ... then (a, b, c) when ... then (d, e, f) else (g, h, i)`
     // an exception throws when entering this method.
@@ -422,6 +423,40 @@ protected boolean booleanEquality(SqlCallBinding binding,
     return false;
   }
 
+  /**
+   * Coerces CASE WHEN and COALESCE statement branches to a unified type.
+   * NULLIF returns the same type as the first operand without return type 
coercion.
+   */
+  @Override public boolean caseOrEquivalentCoercion(SqlCallBinding 
callBinding) {
+    if (callBinding.getCall().getKind() == SqlKind.COALESCE) {
+      // For sql statement like: `coalesce(a, b, c)`
+      return coalesceCoercion(callBinding);
+    } else if (callBinding.getCall().getKind() == SqlKind.NULLIF) {
+      // For sql statement like: `nullif(a, b)`
+      return false;
+    } else {
+      assert callBinding.getCall() instanceof SqlCase;
+      return caseWhenCoercion(callBinding);
+    }
+  }
+
+  /**
+   * COALESCE type coercion, collect all the branches types to find a common 
type,
+   * then cast the operands to the common type when needed.
+   */
+  private boolean coalesceCoercion(SqlCallBinding callBinding) {
+    List<RelDataType> argTypes = new ArrayList<>();
+    SqlValidatorScope scope = getScope(callBinding);
+    for (SqlNode node : callBinding.operands()) {
+      argTypes.add(validator.deriveType(scope, node));
+    }
+    RelDataType widerType = getWiderTypeFor(argTypes, true);
+    if (null != widerType) {
+      return coerceOperandsType(scope, callBinding.getCall(), widerType);
+    }
+    return false;
+  }
+
   /**
    * {@inheritDoc}
    *
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 7be6bcf4b2..19ef2b0347 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -704,6 +704,16 @@ static SqlOperatorTable operatorTableFor(SqlLibrary 
library) {
         .columnType("DECIMAL(5, 2)");
     expr("nullif(345.21, 2e0)")
         .columnType("DECIMAL(5, 2)");
+    expr("nullif(DATE '2020-01-01', '2020-01-01')")
+        .columnType("DATE");
+    expr("nullif('2020-01-01', DATE '2020-01-01')")
+        .columnType("CHAR(10)");
+    expr("nullif(DATE '2020-01-01', '2020-01-01')")
+        .withValidatorConfig(c -> c.withCallRewrite(false))
+        .columnType("DATE");
+    expr("nullif('2020-01-01', DATE '2020-01-01')")
+        .withValidatorConfig(c -> c.withCallRewrite(false))
+        .columnType("CHAR(10)");
     wholeExpr("nullif(1,2,3)")
         .fails("Invalid number of arguments to function 'NULLIF'. Was "
             + "expecting 2 arguments");
@@ -713,7 +723,11 @@ static SqlOperatorTable operatorTableFor(SqlLibrary 
library) {
     expr("coalesce('a','b')").ok();
     expr("coalesce('a','b','c')")
         .columnType("CHAR(1) NOT NULL");
-
+    expr("COALESCE(DATE '2020-01-01', '2020-01-02')")
+        .columnType("VARCHAR NOT NULL");
+    expr("COALESCE(DATE '2020-01-01', '2020-01-02')")
+        .withValidatorConfig(c -> c.withCallRewrite(false))
+        .columnType("VARCHAR NOT NULL");
     sql("select COALESCE(mgr, 12) as m from EMP")
         .columnType("INTEGER NOT NULL");
   }

Reply via email to