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) {