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

jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit b0420947ddeb2c9e18afe896dbf7adaf49a715a2
Author: Oliver Lee <oliver...@google.com>
AuthorDate: Wed Apr 19 22:23:15 2023 +0000

    [CALCITE-5662] Allow CAST(BOOLEAN as INTEGER) (if enabled by conformance)
    
    Conformance method is `SqlConformance.allowLenientCoercion()`,
    formerly `SqlConformance.allowCoercionStringToArray`; it
    also enables coercion of a string to an array, and BOOLEAN
    to other exact numeric types (TINYINT, SMALLINT, INTEGER, BIGINT).
    
    Update reference.md docs so that `CAST(BOOLEAN AS _)`
    reflects SQL standard
    
    Add `SqlTypeMappingRule` as optional parameter to
    `RelDataTypeFactory.leastRestrictive(List<RelDataType>)` so
    that we have access to the mapping rule at call sites of
    `SqlTypeUtil.canCastFrom()`.
    
    Close apache/calcite#3168
---
 .../calcite/rel/type/RelDataTypeFactory.java       | 22 +++++++++-
 .../calcite/rel/type/RelDataTypeFactoryImpl.java   | 11 +++--
 .../java/org/apache/calcite/rex/RexBuilder.java    |  6 +++
 .../apache/calcite/sql/fun/SqlCastFunction.java    | 13 ++++--
 .../calcite/sql/type/SqlTypeCoercionRule.java      | 42 ++++++++++++++++++-
 .../calcite/sql/type/SqlTypeFactoryImpl.java       | 22 ++++++----
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   | 48 ++++++++++++++--------
 .../sql/validate/SqlAbstractConformance.java       |  4 +-
 .../calcite/sql/validate/SqlConformance.java       | 16 +++++---
 .../calcite/sql/validate/SqlConformanceEnum.java   |  8 +++-
 .../sql/validate/SqlDelegatingConformance.java     |  5 +--
 .../apache/calcite/sql/validate/SqlValidator.java  |  8 ++++
 .../calcite/sql/validate/SqlValidatorImpl.java     | 35 +++++++++-------
 .../validate/implicit/AbstractTypeCoercion.java    | 31 ++++++++++----
 .../sql/validate/implicit/TypeCoercionImpl.java    |  4 +-
 .../apache/calcite/sql/type/SqlTypeUtilTest.java   | 23 +++++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 16 ++++++--
 site/_docs/reference.md                            |  2 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   | 20 +++++++++
 19 files changed, 263 insertions(+), 73 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java 
b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
index 182422e4cc..cf24db0b1b 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
@@ -20,6 +20,8 @@ import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
+import org.apache.calcite.sql.type.SqlTypeMappingRules;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 
@@ -196,6 +198,19 @@ public interface RelDataTypeFactory {
   /** Returns the default {@link Charset} (valid if this is a string type). */
   Charset getDefaultCharset();
 
+  /**
+   * Returns the most general of a set of types
+   * using the default type mapping rule.
+   *
+   * @see #leastRestrictive(List, SqlTypeMappingRule)
+   *
+   * @param types input types to be combined using union (not null, not empty)
+   * @return canonical union type descriptor
+   */
+  default @Nullable RelDataType leastRestrictive(List<RelDataType> types) {
+    return leastRestrictive(types, SqlTypeMappingRules.instance(false));
+  }
+
   /**
    * Returns the most general of a set of types (that is, one type to which
    * they can all be cast), or null if conversion is not possible. The result
@@ -203,10 +218,15 @@ public interface RelDataTypeFactory {
    * e.g. <code>leastRestrictive(INT, NUMERIC(3, 2))</code> could be
    * {@code NUMERIC(12, 2)}.
    *
+   * <p>Accepts a {@link SqlTypeMappingRule} that can be used to change casting
+   * behavior.
+   *
    * @param types input types to be combined using union (not null, not empty)
+   * @param mappingRule rule that determines whether types are convertible
    * @return canonical union type descriptor
    */
-  @Nullable RelDataType leastRestrictive(List<RelDataType> types);
+  @Nullable RelDataType leastRestrictive(List<RelDataType> types,
+      SqlTypeMappingRule mappingRule);
 
   /**
    * Creates a SQL type with no precision or scale.
diff --git 
a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java 
b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
index d73339c50e..bdf963f207 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
@@ -24,6 +24,7 @@ import 
org.apache.calcite.sql.type.JavaToSqlTypeConversionRules;
 import org.apache.calcite.sql.type.MapSqlType;
 import org.apache.calcite.sql.type.MultisetSqlType;
 import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.util.Util;
@@ -49,6 +50,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import static java.util.Objects.requireNonNull;
 
 /**
@@ -215,9 +218,11 @@ public abstract class RelDataTypeFactoryImpl implements 
RelDataTypeFactory {
         }, nullable);
   }
 
-  @Override public @Nullable RelDataType leastRestrictive(List<RelDataType> 
types) {
-    assert types != null;
-    assert types.size() >= 1;
+  @Override public @Nullable RelDataType leastRestrictive(
+      List<RelDataType> types, SqlTypeMappingRule mappingRule) {
+    requireNonNull(types, "types");
+    requireNonNull(mappingRule, "mappingRule");
+    checkArgument(types.size() >= 1, "types.size >= 1");
     RelDataType type0 = types.get(0);
     if (type0.isStruct()) {
       return leastRestrictiveStructuredType(types);
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java 
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index fda55faa0c..df55de6b04 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -565,6 +565,12 @@ public class RexBuilder {
       RexLiteral literal = (RexLiteral) exp;
       Comparable value = literal.getValueAs(Comparable.class);
       SqlTypeName typeName = literal.getTypeName();
+
+      // Allow casting boolean literals to integer types.
+      if (exp.getType().getSqlTypeName() == SqlTypeName.BOOLEAN
+          && SqlTypeUtil.isExactNumeric(type)) {
+        return makeCastBooleanToExact(type, exp);
+      }
       if (canRemoveCastFromLiteral(type, value, typeName)) {
         switch (typeName) {
         case INTERVAL_YEAR:
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
index 04a05e7358..ddc30ab295 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
@@ -36,8 +36,10 @@ import org.apache.calcite.sql.type.InferTypes;
 import org.apache.calcite.sql.type.SqlOperandCountRanges;
 import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
+import org.apache.calcite.sql.validate.SqlValidator;
 
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.SetMultimap;
@@ -211,10 +213,13 @@ public class SqlCastFunction extends SqlFunction {
         || left instanceof SqlDynamicParam) {
       return true;
     }
-    RelDataType validatedNodeType =
-        callBinding.getValidator().getValidatedNodeType(left);
-    RelDataType returnType = SqlTypeUtil.deriveType(callBinding, right);
-    if (!SqlTypeUtil.canCastFrom(returnType, validatedNodeType, true)) {
+    final SqlValidator validator = callBinding.getValidator();
+    final RelDataType validatedNodeType =
+        validator.getValidatedNodeType(left);
+    final RelDataType returnType = SqlTypeUtil.deriveType(callBinding, right);
+    final SqlTypeMappingRule mappingRule = validator.getTypeMappingRule();
+
+    if (!SqlTypeUtil.canCastFrom(returnType, validatedNodeType, mappingRule)) {
       if (throwOnFailure) {
         throw callBinding.newError(
             RESOURCE.cannotCastValue(validatedNodeType.toString(),
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
index 7a01fdae97..c54a7949bd 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeCoercionRule.java
@@ -75,6 +75,8 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
 
   private static final SqlTypeCoercionRule INSTANCE;
 
+  private static final SqlTypeCoercionRule LENIENT_INSTANCE;
+
   public static final ThreadLocal<@Nullable SqlTypeCoercionRule> 
THREAD_PROVIDERS =
       ThreadLocal.withInitial(() -> SqlTypeCoercionRule.INSTANCE);
 
@@ -104,7 +106,7 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     final Set<SqlTypeName> rule = new HashSet<>();
 
     // Make numbers symmetrical,
-    // and make VARCHAR, CHAR, BOOLEAN and TIMESTAMP castable to/from numbers
+    // and make VARCHAR, CHAR, and TIMESTAMP castable to/from numbers
     rule.add(SqlTypeName.TINYINT);
     rule.add(SqlTypeName.SMALLINT);
     rule.add(SqlTypeName.INTEGER);
@@ -116,7 +118,6 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
 
     rule.add(SqlTypeName.CHAR);
     rule.add(SqlTypeName.VARCHAR);
-    rule.add(SqlTypeName.BOOLEAN);
     rule.add(SqlTypeName.TIMESTAMP);
     rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
 
@@ -273,6 +274,38 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
             .build());
 
     INSTANCE = new SqlTypeCoercionRule(coerceRules.map);
+
+    // Lenient casting allowing casting between BOOLEAN and numbers.
+    rule.clear();
+
+    rule.add(SqlTypeName.TINYINT);
+    rule.add(SqlTypeName.SMALLINT);
+    rule.add(SqlTypeName.INTEGER);
+    rule.add(SqlTypeName.BIGINT);
+    rule.add(SqlTypeName.DECIMAL);
+    rule.add(SqlTypeName.FLOAT);
+    rule.add(SqlTypeName.REAL);
+    rule.add(SqlTypeName.DOUBLE);
+
+    rule.add(SqlTypeName.CHAR);
+    rule.add(SqlTypeName.VARCHAR);
+    rule.add(SqlTypeName.BOOLEAN);
+    rule.add(SqlTypeName.TIMESTAMP);
+    rule.add(SqlTypeName.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+
+    coerceRules.add(SqlTypeName.TINYINT, rule);
+    coerceRules.add(SqlTypeName.SMALLINT, rule);
+    coerceRules.add(SqlTypeName.INTEGER, rule);
+    coerceRules.add(SqlTypeName.BIGINT, rule);
+
+    // Lenient casting allowing ARRAY to be casted from CHAR and VARCHAR.
+    coerceRules.add(SqlTypeName.ARRAY,
+        coerceRules.copyValues(SqlTypeName.ARRAY)
+            .add(SqlTypeName.CHAR)
+            .add(SqlTypeName.VARCHAR)
+            .build());
+
+    LENIENT_INSTANCE = new SqlTypeCoercionRule(coerceRules.map);
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -282,6 +315,11 @@ public class SqlTypeCoercionRule implements 
SqlTypeMappingRule {
     return Objects.requireNonNull(THREAD_PROVIDERS.get(), "threadProviders");
   }
 
+  /** Returns an instance that allows more lenient type coercion. */
+  public static SqlTypeCoercionRule lenientInstance() {
+    return LENIENT_INSTANCE;
+  }
+
   /** Returns an instance with specified type mappings. */
   public static SqlTypeCoercionRule instance(
       Map<SqlTypeName, ImmutableSet<SqlTypeName>> map) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
index b414e6787e..12a560402d 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
@@ -30,6 +30,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
 import java.nio.charset.Charset;
 import java.util.List;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import static java.util.Objects.requireNonNull;
 
 /**
@@ -156,9 +158,12 @@ public class SqlTypeFactoryImpl extends 
RelDataTypeFactoryImpl {
     return canonize(newType);
   }
 
-  @Override public @Nullable RelDataType leastRestrictive(List<RelDataType> 
types) {
-    assert types != null;
-    assert types.size() >= 1;
+  @Override public @Nullable RelDataType leastRestrictive(
+      List<RelDataType> types,
+      SqlTypeMappingRule mappingRule) {
+    requireNonNull(types, "types");
+    requireNonNull(mappingRule, "mappingRule");
+    checkArgument(types.size() >= 1, "types.size >= 1");
 
     RelDataType type0 = types.get(0);
     if (type0.getSqlTypeName() != null) {
@@ -166,13 +171,14 @@ public class SqlTypeFactoryImpl extends 
RelDataTypeFactoryImpl {
       if (resultType != null) {
         return resultType;
       }
-      return leastRestrictiveByCast(types);
+      return leastRestrictiveByCast(types, mappingRule);
     }
 
-    return super.leastRestrictive(types);
+    return super.leastRestrictive(types, mappingRule);
   }
 
-  private @Nullable RelDataType leastRestrictiveByCast(List<RelDataType> 
types) {
+  private @Nullable RelDataType leastRestrictiveByCast(List<RelDataType> types,
+      SqlTypeMappingRule mappingRule) {
     RelDataType resultType = types.get(0);
     boolean anyNullable = resultType.isNullable();
     for (int i = 1; i < types.size(); i++) {
@@ -186,10 +192,10 @@ public class SqlTypeFactoryImpl extends 
RelDataTypeFactoryImpl {
         anyNullable = true;
       }
 
-      if (SqlTypeUtil.canCastFrom(type, resultType, false)) {
+      if (SqlTypeUtil.canCastFrom(type, resultType, mappingRule)) {
         resultType = type;
       } else {
-        if (!SqlTypeUtil.canCastFrom(resultType, type, false)) {
+        if (!SqlTypeUtil.canCastFrom(resultType, type, mappingRule)) {
           return null;
         }
       }
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
index aef3fd0725..169e9e3577 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
@@ -846,7 +846,8 @@ public abstract class SqlTypeUtil {
   }
 
   /**
-   * Compares two types and returns true if fromType can be cast to toType.
+   * Compares two types and returns whether {@code fromType} can be cast to
+   * {@code toType}, using either coercion or assignment.
    *
    * <p>REVIEW jvs 17-Dec-2004: the coerce param below shouldn't really be
    * necessary. We're using it as a hack because
@@ -859,12 +860,35 @@ public abstract class SqlTypeUtil {
    * @param coerce   if true, the SQL rules for CAST are used; if false, the
    *                 rules are similar to Java; e.g. you can't assign short x =
    *                 (int) y, and you can't assign int x = (String) z.
-   * @return true iff cast is legal
+   * @return whether cast is legal
    */
   public static boolean canCastFrom(
       RelDataType toType,
       RelDataType fromType,
       boolean coerce) {
+    return canCastFrom(toType, fromType,
+        coerce ? SqlTypeCoercionRule.instance()
+            : SqlTypeAssignmentRule.instance());
+  }
+
+  /**
+   * Compares two types and returns whether {@code fromType} can be cast to
+   * {@code toType}.
+   *
+   * <p>A type mapping rule (i.e. {@link SqlTypeCoercionRule}
+   * or {@link SqlTypeAssignmentRule}) controls what types are allowed to be
+   * cast from/to.
+   *
+   * @param toType   target of assignment
+   * @param fromType source of assignment
+   * @param typeMappingRule  SqlTypeMappingRule
+   *
+   * @return whether cast is legal
+   */
+  public static boolean canCastFrom(
+      RelDataType toType,
+      RelDataType fromType,
+      SqlTypeMappingRule typeMappingRule) {
     if (toType.equals(fromType)) {
       return true;
     }
@@ -884,10 +908,10 @@ public abstract class SqlTypeUtil {
           return false;
         }
         return canCastFrom(
-            toType.getFieldList().get(0).getType(), fromType, coerce);
+            toType.getFieldList().get(0).getType(), fromType, typeMappingRule);
       } else if (fromTypeName == SqlTypeName.DISTINCT) {
         return canCastFrom(
-            toType, fromType.getFieldList().get(0).getType(), coerce);
+            toType, fromType.getFieldList().get(0).getType(), typeMappingRule);
       } else if (toTypeName == SqlTypeName.ROW) {
         if (fromTypeName != SqlTypeName.ROW) {
           return fromTypeName == SqlTypeName.NULL;
@@ -902,7 +926,7 @@ public abstract class SqlTypeUtil {
           if (!canCastFrom(
               toField.getType(),
               fromField.getType(),
-              coerce)) {
+              typeMappingRule)) {
             return false;
           }
         }
@@ -917,7 +941,7 @@ public abstract class SqlTypeUtil {
         return canCastFrom(
             getComponentTypeOrThrow(toType),
             getComponentTypeOrThrow(fromType),
-            coerce);
+            typeMappingRule);
       } else if (fromTypeName == SqlTypeName.MULTISET) {
         return false;
       } else {
@@ -928,7 +952,7 @@ public abstract class SqlTypeUtil {
     if (c1 != null) {
       RelDataType c2 = fromType.getComponentType();
       if (c2 != null) {
-        return canCastFrom(c1, c2, coerce);
+        return canCastFrom(c1, c2, typeMappingRule);
       }
     }
     if ((isInterval(fromType) && isExactNumeric(toType))
@@ -944,15 +968,7 @@ public abstract class SqlTypeUtil {
     if (toTypeName == null || fromTypeName == null) {
       return false;
     }
-
-    // REVIEW jvs 9-Feb-2009: we don't impose SQL rules for character sets
-    // here; instead, we do that in SqlCastFunction.  The reason is that
-    // this method is called from at least one place (MedJdbcNameDirectory)
-    // where internally a cast across character repertoires is OK.  Should
-    // probably clean that up.
-
-    SqlTypeMappingRule rules = SqlTypeMappingRules.instance(coerce);
-    return rules.canApplyFrom(toTypeName, fromTypeName);
+    return typeMappingRule.canApplyFrom(toTypeName, fromTypeName);
   }
 
   /**
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
index c4e456dedf..3205a48b40 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java
@@ -141,7 +141,7 @@ public abstract class SqlAbstractConformance implements 
SqlConformance {
     return SqlConformanceEnum.DEFAULT.semantics();
   }
 
-  @Override public boolean allowCoercionStringToArray() {
-    return SqlConformanceEnum.DEFAULT.allowCoercionStringToArray();
+  @Override public boolean allowLenientCoercion() {
+    return SqlConformanceEnum.DEFAULT.allowLenientCoercion();
   }
 }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
index eef63adefa..0a400da21a 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java
@@ -563,17 +563,23 @@ public interface SqlConformance {
   SqlLibrary semantics();
 
   /**
-   * Whether to allow coercion string literal to array literal
+   * Whether to allow lenient type coercions.
    *
-   * <p>For example,
+   * <p>Coercions include:
+   * <ul>
+   *
+   * <li>Coercion of string literal to array literal. For example,
+   * {@code SELECT ARRAY[0,1,2] == '{0,1,2}'}
    *
-   * <blockquote><pre>SELECT ARRAY[0,1,2] == '{0,1,2}'
-   * </pre></blockquote>
+   * <li>Casting {@code BOOLEAN} values to one of the following numeric types:
+   * {@code TINYINT}, {@code SMALLINT}, {@code INTEGER}, {@code BIGINT}.
+   *
+   * </ul>
    *
    * <p>Among the built-in conformance levels, true in
    * {@link SqlConformanceEnum#BABEL},
    * false otherwise.
    */
   @Experimental
-  boolean allowCoercionStringToArray();
+  boolean allowLenientCoercion();
 }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
index 5ff7802718..a610f19f16 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java
@@ -431,9 +431,15 @@ public enum SqlConformanceEnum implements SqlConformance {
     }
   }
 
-  @Override public boolean allowCoercionStringToArray() {
+  @Override public boolean allowLenientCoercion() {
+    /* This allows for the following:
+     - coercion from string to array
+     - coercion from boolean to integers
+     */
     switch (this) {
     case BABEL:
+    case BIG_QUERY:
+    case MYSQL_5:
       return true;
     default:
       return false;
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
index 0dea730551..c8e2f7cdd2 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java
@@ -147,8 +147,7 @@ public class SqlDelegatingConformance implements 
SqlConformance {
     return delegate.semantics();
   }
 
-  @Override public boolean allowCoercionStringToArray() {
-    return delegate.allowCoercionStringToArray();
+  @Override public boolean allowLenientCoercion() {
+    return delegate.allowLenientCoercion();
   }
-
 }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index 4c05824ce7..d20d9f4a49 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -46,6 +46,7 @@ import org.apache.calcite.sql.SqlWindow;
 import org.apache.calcite.sql.SqlWith;
 import org.apache.calcite.sql.SqlWithItem;
 import org.apache.calcite.sql.type.SqlTypeCoercionRule;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
 import org.apache.calcite.sql.validate.implicit.TypeCoercion;
 import org.apache.calcite.sql.validate.implicit.TypeCoercionFactory;
 import org.apache.calcite.sql.validate.implicit.TypeCoercions;
@@ -778,6 +779,13 @@ public interface SqlValidator {
   /** Get the type coercion instance. */
   TypeCoercion getTypeCoercion();
 
+  /** Returns the type mapping rule. */
+  default SqlTypeMappingRule getTypeMappingRule() {
+    return config().conformance().allowLenientCoercion()
+        ? SqlTypeCoercionRule.lenientInstance()
+        : SqlTypeCoercionRule.instance();
+  }
+
   /** Returns the config of the validator. */
   Config config();
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index a16d30862f..9e42e9dca9 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -279,7 +279,7 @@ public class SqlValidatorImpl implements 
SqlValidatorWithHints {
       new SqlValidatorImpl.ValidationErrorFunction();
 
   // TypeCoercion instance used for implicit type coercion.
-  private TypeCoercion typeCoercion;
+  private final TypeCoercion typeCoercion;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -322,21 +322,28 @@ public class SqlValidatorImpl implements 
SqlValidatorWithHints {
     TypeCoercion typeCoercion = 
config.typeCoercionFactory().create(typeFactory, this);
     this.typeCoercion = typeCoercion;
 
-    if (config.conformance().allowCoercionStringToArray()) {
-      SqlTypeCoercionRule rules = requireNonNull(config.typeCoercionRules() != 
null
-          ? config.typeCoercionRules() : 
SqlTypeCoercionRule.THREAD_PROVIDERS.get());
-
-      ImmutableSet<SqlTypeName> arrayMapping = 
ImmutableSet.<SqlTypeName>builder()
-          .addAll(rules.getTypeMapping().getOrDefault(SqlTypeName.ARRAY, 
ImmutableSet.of()))
-          .add(SqlTypeName.VARCHAR)
-          .add(SqlTypeName.CHAR)
-          .build();
-
-      Map<SqlTypeName, ImmutableSet<SqlTypeName>> mapping = new 
HashMap(rules.getTypeMapping());
+    if (config.conformance().allowLenientCoercion()) {
+      final SqlTypeCoercionRule rules =
+          requireNonNull(
+              config.typeCoercionRules() != null
+                  ? config.typeCoercionRules()
+                  : SqlTypeCoercionRule.THREAD_PROVIDERS.get(),
+              "rules");
+
+      final ImmutableSet<SqlTypeName> arrayMapping =
+          ImmutableSet.<SqlTypeName>builder()
+              .addAll(rules.getTypeMapping()
+                  .getOrDefault(SqlTypeName.ARRAY, ImmutableSet.of()))
+              .add(SqlTypeName.VARCHAR)
+              .add(SqlTypeName.CHAR)
+              .build();
+
+      Map<SqlTypeName, ImmutableSet<SqlTypeName>> mapping =
+          new HashMap<>(rules.getTypeMapping());
       mapping.replace(SqlTypeName.ARRAY, arrayMapping);
-      rules = SqlTypeCoercionRule.instance(mapping);
+      SqlTypeCoercionRule rules2 = SqlTypeCoercionRule.instance(mapping);
 
-      SqlTypeCoercionRule.THREAD_PROVIDERS.set(rules);
+      SqlTypeCoercionRule.THREAD_PROVIDERS.set(rules2);
     } else if (config.typeCoercionRules() != null) {
       SqlTypeCoercionRule.THREAD_PROVIDERS.set(config.typeCoercionRules());
     }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
index 82b7c52343..939a3c74c1 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
@@ -34,7 +34,9 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.parser.SqlParserUtil;
+import org.apache.calcite.sql.type.SqlTypeCoercionRule;
 import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
@@ -112,7 +114,8 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
     }
 
     // Check it early.
-    if (!needToCast(scope, operand, targetType)) {
+    if (!needToCast(scope, operand, targetType,
+        SqlTypeCoercionRule.lenientInstance())) {
       return false;
     }
     // Fix up nullable attr.
@@ -238,9 +241,20 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
     return syncedType;
   }
 
-  /** Decide if a SqlNode should be casted to target type, derived class
-   * can override this strategy. */
-  protected boolean needToCast(SqlValidatorScope scope, SqlNode node, 
RelDataType toType) {
+  /** Returns whether a SqlNode should be cast to a target type.
+   *
+   * <p>The default implementation uses the scope's validator's type mapping
+   * rule to determine validity; a derived class can override this strategy. */
+  protected boolean needToCast(SqlValidatorScope scope, SqlNode node,
+      RelDataType toType) {
+    return needToCast(scope, node, toType,
+        scope.getValidator().getTypeMappingRule());
+  }
+
+  /** Returns whether a SqlNode should be cast to a target type,
+   * using a type mapping rule to determine casting validity. */
+  protected boolean needToCast(SqlValidatorScope scope, SqlNode node,
+      RelDataType toType, SqlTypeMappingRule mappingRule) {
     RelDataType fromType = validator.deriveType(scope, node);
     // This depends on the fact that type validate happens before coercion.
     // We do not have inferred type for some node, i.e. LOCALTIME.
@@ -261,7 +275,8 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
     }
 
     // No need to cast between char and varchar.
-    if (SqlTypeUtil.isCharacter(toType) && SqlTypeUtil.isCharacter(fromType)) {
+    if (SqlTypeUtil.isCharacter(toType)
+        && SqlTypeUtil.isCharacter(fromType)) {
       return false;
     }
 
@@ -279,7 +294,7 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
       return false;
     }
     // Should keep sync with rules in SqlTypeCoercionRule.
-    assert SqlTypeUtil.canCastFrom(toType, fromType, true);
+    assert SqlTypeUtil.canCastFrom(toType, fromType, mappingRule);
     return true;
   }
 
@@ -534,7 +549,7 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
       return type2;
     }
 
-    if (validator.config().conformance().allowCoercionStringToArray()) {
+    if (validator.config().conformance().allowLenientCoercion()) {
       if (SqlTypeUtil.isString(type1) && SqlTypeUtil.isArray(type2)) {
         return type2;
       }
@@ -754,7 +769,7 @@ public abstract class AbstractTypeCoercion implements 
TypeCoercion {
       int index,
       RelDataType fromType,
       RelDataType targetType) {
-    if (validator.config().conformance().allowCoercionStringToArray()
+    if (validator.config().conformance().allowLenientCoercion()
         && SqlTypeUtil.isString(fromType)
         && SqlTypeUtil.isArray(targetType)
         && operand instanceof SqlCharStringLiteral) {
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 bee2d5931f..e51da487dc 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
@@ -37,6 +37,7 @@ import org.apache.calcite.sql.fun.SqlCase;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlOperandMetadata;
 import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeMappingRule;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
@@ -630,11 +631,12 @@ public class TypeCoercionImpl extends 
AbstractTypeCoercion {
     final List<RelDataTypeField> sourceFields = sourceRowType.getFieldList();
     final List<RelDataTypeField> targetFields = targetRowType.getFieldList();
     final int sourceCount = sourceFields.size();
+    SqlTypeMappingRule mappingRule = validator.getTypeMappingRule();
     for (int i = 0; i < sourceCount; i++) {
       RelDataType sourceType = sourceFields.get(i).getType();
       RelDataType targetType = targetFields.get(i).getType();
       if (!SqlTypeUtil.equalSansNullability(validator.getTypeFactory(), 
sourceType, targetType)
-          && !SqlTypeUtil.canCastFrom(targetType, sourceType, true)) {
+          && !SqlTypeUtil.canCastFrom(targetType, sourceType, mappingRule)) {
         // Returns early if types not equals and can not do type coercion.
         return false;
       }
diff --git 
a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java 
b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
index a2c3fe1ad9..c72414c04c 100644
--- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
@@ -122,9 +122,13 @@ class SqlTypeUtilTest {
     SqlTypeCoercionRule typeCoercionRules = 
SqlTypeCoercionRule.instance(builder.map);
     assertThat(SqlTypeUtil.canCastFrom(f.sqlTimestampPrec3, f.sqlBoolean, 
true),
         is(false));
+    assertThat(SqlTypeUtil.canCastFrom(f.sqlTimestampPrec3, f.sqlBoolean, 
defaultRules),
+        is(false));
     SqlTypeCoercionRule.THREAD_PROVIDERS.set(typeCoercionRules);
     assertThat(SqlTypeUtil.canCastFrom(f.sqlTimestampPrec3, f.sqlBoolean, 
true),
         is(true));
+    assertThat(SqlTypeUtil.canCastFrom(f.sqlTimestampPrec3, f.sqlBoolean, 
typeCoercionRules),
+        is(true));
     // Recover the mappings to default.
     SqlTypeCoercionRule.THREAD_PROVIDERS.set(defaultRules);
   }
@@ -253,7 +257,22 @@ class SqlTypeUtilTest {
     }
   }
 
+  /** Tests that casting BOOLEAN to INTEGER is not allowed for the default
+   * {@link SqlTypeCoercionRule}, but is allowed in lenient mode. */
+  @Test void testCastBooleanToInteger() {
+    RelDataType booleanType = f.sqlBoolean;
+    RelDataType intType = f.sqlInt;
+    final SqlTypeCoercionRule rule = SqlTypeCoercionRule.instance();
+    final SqlTypeCoercionRule lenientRule =
+        SqlTypeCoercionRule.lenientInstance();
+    assertThat(SqlTypeUtil.canCastFrom(intType, booleanType, rule),
+        is(false));
+    assertThat(SqlTypeUtil.canCastFrom(intType, booleanType, lenientRule),
+        is(true));
+  }
+
   private static void assertCanCast(RelDataType toType, RelDataType fromType) {
+    final SqlTypeCoercionRule defaultRules = SqlTypeCoercionRule.instance();
     assertThat(
         String.format(Locale.ROOT,
             "Expected to be able to cast from %s to %s without coercion.", 
fromType, toType),
@@ -262,5 +281,9 @@ class SqlTypeUtilTest {
         String.format(Locale.ROOT,
             "Expected to be able to cast from %s to %s with coercion.", 
fromType, toType),
         SqlTypeUtil.canCastFrom(toType, fromType, /* coerce= */ true), 
is(true));
+    assertThat(
+        String.format(Locale.ROOT,
+            "Expected to be able to cast from %s to %s without coercion.", 
fromType, toType),
+        SqlTypeUtil.canCastFrom(toType, fromType, /* coerce= */ defaultRules), 
is(true));
   }
 }
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 6e6119c900..e58c7b29df 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -1270,8 +1270,9 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         .columnType("BOOLEAN NOT NULL");
     expr("cast(1.0e1 as boolean)")
         .columnType("BOOLEAN NOT NULL");
-    expr("cast(true as numeric)")
-        .columnType("DECIMAL(19, 0) NOT NULL");
+    expr("^cast(true as numeric)^")
+        .fails("Cast function cannot convert value of type BOOLEAN "
+        + "to type DECIMAL\\(19, 0\\)");
     // It's a runtime error that 'TRUE' cannot fit into CHAR(3), but at
     // validate time this expression is OK.
     expr("cast(true as char(3))")
@@ -10534,9 +10535,15 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         .withTypeCoercion(false)
         .fails("Cannot assign to target field 'COMM' of type INTEGER"
             + " from source field 'EXPR\\$3' of type BOOLEAN");
+    sql("insert into EMPDEFAULTS(\"comm\" BOOLEAN)"
+        + " (empno, ename, job, ^comm^)\n"
+        + "values (1, 'Arthur', 'clown', true)")
+        .fails("Cannot assign to target field 'COMM' of type INTEGER"
+            + " from source field 'EXPR\\$3' of type BOOLEAN");
     sql("insert into EMPDEFAULTS(\"comm\" BOOLEAN)"
         + " (empno, ename, job, comm)\n"
         + "values (1, 'Arthur', 'clown', true)")
+        .withConformance(SqlConformanceEnum.BIG_QUERY)
         .ok();
     sql("insert into EMPDEFAULTS(\"comm\" BOOLEAN)"
         + " (empno, ename, job, ^\"comm\"^)\n"
@@ -10571,7 +10578,7 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
     final String error2 = "Cannot assign to target field 'slacker' of type"
         + " INTEGER from source field 'EXPR\\$3' of type BOOLEAN";
     s.withSql(sql2).withTypeCoercion(false).fails(error2);
-    s.withSql(sql2).ok();
+    s.withConformance(SqlConformanceEnum.BIG_QUERY).withSql(sql2).ok();
   }
 
   @Test void testInsertExtendedColumnModifiableViewFailExtendedCollision() {
@@ -10602,7 +10609,8 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
     final String sql4 = "insert into EMP_MODIFIABLEVIEW2(\"extra\" INTEGER)"
         + " (empno, ename, job, \"extra\")\n"
         + "values (1, 'Arthur', 'clown', true)";
-    s.withSql(sql4).ok();
+    s.withConformance(SqlConformanceEnum.BIG_QUERY).withTypeCoercion(true)
+        .withSql(sql4).ok();
   }
 
   @Test void testInsertExtendedColumnModifiableViewFailUnderlyingCollision() {
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index ea611d449e..077ecb75c3 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1556,7 +1556,7 @@ these details follow the table.
 | FROM - TO           | NULL | BOOLEAN | TINYINT | SMALLINT | INT | BIGINT | 
DECIMAL | FLOAT or REAL | DOUBLE | INTERVAL | DATE | TIME | TIMESTAMP | CHAR or 
VARCHAR | BINARY or VARBINARY | GEOMETRY | ARRAY |
 
|:--------------------|:-----|:--------|:--------|:---------|:----|:-------|:--------|:--------------|:-------|:---------|:-----|:-----|:----------|:----------------|:--------------------|:---------|:------|
 | NULL                | i    | i       | i       | i        | i   | i      | i 
      | i             | i      | i        | i    | i    | i         | i         
      | i                   | i        | x     |
-| BOOLEAN             | x    | i       | e       | e        | e   | e      | e 
      | e             | e      | x        | x    | x    | x         | i         
      | x                   | x        | x     |
+| BOOLEAN             | x    | i       | x       | x        | x   | x      | x 
      | x             | x      | x        | x    | x    | x         | i         
      | x                   | x        | x     |
 | TINYINT             | x    | e       | i       | i        | i   | i      | i 
      | i             | i      | e        | x    | x    | e         | i         
      | x                   | x        | x     |
 | SMALLINT            | x    | e       | i       | i        | i   | i      | i 
      | i             | i      | e        | x    | x    | e         | i         
      | x                   | x        | x     |
 | INT                 | x    | e       | i       | i        | i   | i      | i 
      | i             | i      | e        | x    | x    | e         | i         
      | x                   | x        | x     |
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 0f2b16af03..57585050b4 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -570,6 +570,26 @@ public class SqlOperatorTest {
     f.checkString("cast(false as varchar(4))", "FALS", "VARCHAR(4) NOT NULL");
   }
 
+  @ParameterizedTest
+  @MethodSource("safeParameters")
+  void testCastBooleanToNumeric(CastType castType, SqlOperatorFixture f) {
+    f.setFor(SqlStdOperatorTable.CAST, VmName.EXPAND);
+    SqlOperatorFixture f0 = f.withConformance(SqlConformanceEnum.DEFAULT);
+    f0.checkFails("^" + castType.name() + "(true as integer)^",
+        "Cast function cannot convert value of type BOOLEAN to type INTEGER", 
false);
+    f0.checkFails("^" + castType.name() + "(true as decimal)^",
+        "Cast function cannot convert value of type BOOLEAN to type 
DECIMAL\\(19, 0\\)", false);
+
+    SqlOperatorFixture f1 = f.withConformance(SqlConformanceEnum.BIG_QUERY);
+    f1.checkString("cast(true as integer)", "1", "INTEGER NOT NULL");
+    f1.checkString("cast(false as integer)", "0", "INTEGER NOT NULL");
+    f1.checkString("cast(true as bigint)", "1", "BIGINT NOT NULL");
+    f1.checkFails("^" + castType.name() + "(true as float)^",
+        "Cast function cannot convert value of type BOOLEAN to type FLOAT", 
false);
+    f1.checkFails("^" + castType.name() + "(true as decimal)^",
+        "Cast function cannot convert value of type BOOLEAN to type 
DECIMAL\\(19, 0\\)", false);
+  }
+
   @ParameterizedTest
   @MethodSource("safeParameters")
   void testCastExactNumericLimits(CastType castType, SqlOperatorFixture f) {


Reply via email to