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 c228804e25 [CALCITE-6389] RexBuilder.removeCastFromLiteral does not 
preserve semantics for some types of literal
c228804e25 is described below

commit c228804e258f72cd5df0f1f263dcc4f86410d3ff
Author: Mihai Budiu <mbu...@feldera.com>
AuthorDate: Tue May 7 14:29:40 2024 -0700

    [CALCITE-6389] RexBuilder.removeCastFromLiteral does not preserve semantics 
for some types of literal
    
    Signed-off-by: Mihai Budiu <mbu...@feldera.com>
---
 .../adapter/enumerable/RexToLixTranslator.java     | 31 ++++++++-----
 .../java/org/apache/calcite/rex/RexBuilder.java    | 37 +++++++++------
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   | 30 ++++++++++++
 .../org/apache/calcite/rex/RexBuilderTest.java     | 53 ++++++++++++++++++++++
 4 files changed, 125 insertions(+), 26 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index e49788c594..7886c5353d 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -268,22 +268,29 @@ public class RexToLixTranslator implements 
RexVisitor<RexToLixTranslator.Result>
   /**
    * Used for safe operators that return null if an exception is thrown.
    */
-  private static Expression expressionHandlingSafe(Expression body, boolean 
safe) {
-    return safe ? safeExpression(body) : body;
+  private Expression expressionHandlingSafe(
+      Expression body, boolean safe, RelDataType targetType) {
+    return safe ? safeExpression(body, targetType) : body;
   }
 
-  private static Expression safeExpression(Expression body) {
+  private Expression safeExpression(Expression body, RelDataType targetType) {
     final ParameterExpression e_ =
         Expressions.parameter(Exception.class, new 
BlockBuilder().newName("e"));
 
-    return Expressions.call(
-        Expressions.lambda(
-            Expressions.block(
-                Expressions.tryCatch(
-                    Expressions.return_(null, body),
-                Expressions.catch_(e_,
-                    Expressions.return_(null, constant(null)))))),
-        BuiltInMethod.FUNCTION0_APPLY.method);
+    // The type received for the targetType is never nullable.
+    // But safe casts may return null
+    RelDataType nullableTargetType = 
typeFactory.createTypeWithNullability(targetType, true);
+    Expression result =
+        Expressions.call(
+            Expressions.lambda(
+                Expressions.block(
+                    Expressions.tryCatch(
+                        Expressions.return_(null, body),
+                        Expressions.catch_(e_,
+                            Expressions.return_(null, constant(null)))))),
+            BuiltInMethod.FUNCTION0_APPLY.method);
+    // FUNCTION0 always returns Object, so we need a cast to the target type
+    return EnumUtils.convert(result, 
typeFactory.getJavaClass(nullableTargetType));
   }
 
   Expression translateCast(
@@ -294,7 +301,7 @@ public class RexToLixTranslator implements 
RexVisitor<RexToLixTranslator.Result>
       ConstantExpression format) {
     Expression convert = getConvertExpression(sourceType, targetType, operand, 
format);
     Expression convert2 = checkExpressionPadTruncate(convert, sourceType, 
targetType);
-    Expression convert3 = expressionHandlingSafe(convert2, safe);
+    Expression convert3 = expressionHandlingSafe(convert2, safe, targetType);
     return scaleValue(sourceType, targetType, convert3);
   }
 
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 341759f57b..7a7f247749 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -694,7 +694,9 @@ public class RexBuilder {
       return false;
     }
     if (toType.getSqlTypeName() != fromTypeName
-        && SqlTypeFamily.DATETIME.getTypeNames().contains(fromTypeName)) {
+        && (SqlTypeFamily.DATETIME.getTypeNames().contains(fromTypeName)
+        || 
SqlTypeFamily.INTERVAL_DAY_TIME.getTypeNames().contains(fromTypeName)
+        || 
SqlTypeFamily.INTERVAL_YEAR_MONTH.getTypeNames().contains(fromTypeName))) {
       return false;
     }
     if (value instanceof NlsString) {
@@ -720,9 +722,10 @@ public class RexBuilder {
       }
     }
 
-    if (toType.getSqlTypeName() == SqlTypeName.DECIMAL) {
+    if (toType.getSqlTypeName() == SqlTypeName.DECIMAL
+        && fromTypeName.getFamily() == SqlTypeFamily.NUMERIC) {
       final BigDecimal decimalValue = (BigDecimal) value;
-      return SqlTypeUtil.isValidDecimalValue(decimalValue, toType);
+      return SqlTypeUtil.canBeRepresentedExactly(decimalValue, toType);
     }
 
     if (SqlTypeName.INT_TYPES.contains(sqlType)) {
@@ -731,17 +734,23 @@ public class RexBuilder {
       if (s != 0) {
         return false;
       }
-      long l = decimalValue.longValue();
-      switch (sqlType) {
-      case TINYINT:
-        return l >= Byte.MIN_VALUE && l <= Byte.MAX_VALUE;
-      case SMALLINT:
-        return l >= Short.MIN_VALUE && l <= Short.MAX_VALUE;
-      case INTEGER:
-        return l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE;
-      case BIGINT:
-      default:
-        return true;
+      try {
+        // will trigger ArithmeticException when the value
+        // cannot be represented exactly as a long
+        long l = decimalValue.longValueExact();
+        switch (sqlType) {
+        case TINYINT:
+          return l >= Byte.MIN_VALUE && l <= Byte.MAX_VALUE;
+        case SMALLINT:
+          return l >= Short.MIN_VALUE && l <= Short.MAX_VALUE;
+        case INTEGER:
+          return l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE;
+        case BIGINT:
+        default:
+          return true;
+        }
+      } catch (ArithmeticException ex) {
+        return false;
       }
     }
 
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 3e91b1b781..7f0bc64516 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
@@ -51,6 +51,7 @@ import 
org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.nio.charset.Charset;
 import java.util.AbstractList;
 import java.util.ArrayList;
@@ -1822,6 +1823,35 @@ public abstract class SqlTypeUtil {
         type.getFieldList().subList(fieldsCnt - numToKeep, fieldsCnt));
   }
 
+  /**
+   * Returns whether the decimal value can be represented without information 
loss
+   * using the specified type.
+   * For example, 1111.11
+   * - cannot be represented exactly using DECIMAL(3, 1) since it overflows.
+   * - cannot be represented exactly using DECIMAL(6, 3) since it overflows.
+   * - cannot be represented exactly using DECIMAL(6, 1) since it requires 
rounding.
+   * - can be represented exactly using DECIMAL(6, 2)
+   *
+   * @param value  A decimal value
+   * @param toType A DECIMAL type.
+   * @return whether the value is valid for the type
+   */
+  public static boolean canBeRepresentedExactly(@Nullable BigDecimal value, 
RelDataType toType) {
+    assert toType.getSqlTypeName() == SqlTypeName.DECIMAL;
+    if (value == null) {
+      return true;
+    }
+    value = value.stripTrailingZeros();
+    if (value.scale() < 0) {
+      // Negative scale, convert to 0 scale.
+      // Rounding mode is irrelevant, since value is integer
+      value = value.setScale(0, RoundingMode.DOWN);
+    }
+    final int intDigits = value.precision() - value.scale();
+    final int maxIntDigits = toType.getPrecision() - toType.getScale();
+    return (intDigits <= maxIntDigits) && (value.scale() <= toType.getScale());
+  }
+
   /**
    * Returns whether the decimal value is valid for the type. For example, 
1111.11 is not
    * valid for DECIMAL(3, 1) since it overflows.
diff --git a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java 
b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
index 9db5f8ed07..535232a105 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexBuilderTest.java
@@ -69,6 +69,7 @@ import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasToString;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -213,6 +214,58 @@ class RexBuilderTest {
         hasToString("1969-07-21 02:56:15.102"));
   }
 
+  /** Test cases for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6389";>[CALCITE-6389]
+   * RexBuilder.removeCastFromLiteral does not preserve semantics for some 
types of literal</a>. */
+  @Test void testRemoveCast() {
+    final RelDataTypeFactory typeFactory = new 
SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
+    RexBuilder builder = new RexBuilder(typeFactory);
+
+    // Can remove cast of an integer to an integer
+    BigDecimal value = new BigDecimal(10);
+    RelDataType toType = 
builder.typeFactory.createSqlType(SqlTypeName.INTEGER);
+    assertTrue(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTEGER));
+
+    // Can remove cast from integer to decimal
+    toType = builder.typeFactory.createSqlType(SqlTypeName.DECIMAL);
+    assertTrue(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTEGER));
+
+    // 250 is too large for a TINYINT
+    value = new BigDecimal(250);
+    toType = builder.typeFactory.createSqlType(SqlTypeName.TINYINT);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTEGER));
+
+    // 50 isn't too large for a TINYINT
+    value = new BigDecimal(50);
+    toType = builder.typeFactory.createSqlType(SqlTypeName.TINYINT);
+    assertTrue(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTEGER));
+
+    // 120.25 cannot be represented with precision 2 and scale 2 without loss
+    value = new BigDecimal("120.25");
+    toType = builder.typeFactory.createSqlType(SqlTypeName.DECIMAL, 2, 2);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.DECIMAL));
+
+    // 120.25 cannot be represented with precision 5 and scale 1 without 
rounding
+    value = new BigDecimal("120.25");
+    toType = builder.typeFactory.createSqlType(SqlTypeName.DECIMAL, 5, 1);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.DECIMAL));
+
+    // longmax + 1 cannot be represented as a long
+    value = new BigDecimal(Long.MAX_VALUE).add(BigDecimal.ONE);
+    toType = builder.typeFactory.createSqlType(SqlTypeName.BIGINT);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.DECIMAL));
+
+    // Cast to decimal of an INTERVAL '5' seconds cannot be removed
+    value = new BigDecimal("5");
+    toType = builder.typeFactory.createSqlType(SqlTypeName.DECIMAL, 5, 1);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTERVAL_SECOND));
+
+    // Cast to decimal of an INTERVAL '5' minutes cannot be removed
+    value = new BigDecimal("5");
+    toType = builder.typeFactory.createSqlType(SqlTypeName.DECIMAL, 5, 1);
+    assertFalse(builder.canRemoveCastFromLiteral(toType, value, 
SqlTypeName.INTERVAL_MINUTE));
+  }
+
   @Test void testTimestampString() {
     final TimestampString ts = new TimestampString(1969, 7, 21, 2, 56, 15);
     assertThat(ts, hasToString("1969-07-21 02:56:15"));

Reply via email to