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

mihaibudiu 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 7c0dfef966 [CALCITE-7443] Incorrect simplification for large interval
7c0dfef966 is described below

commit 7c0dfef96602b320da006c4086aec9ba38edbaef
Author: Mihai Budiu <[email protected]>
AuthorDate: Tue Mar 24 17:55:14 2026 -0700

    [CALCITE-7443] Incorrect simplification for large interval
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../java/org/apache/calcite/prepare/Prepare.java   | 13 ++++--
 .../org/apache/calcite/runtime/SqlFunctions.java   | 20 +++++++-
 .../apache/calcite/sql2rel/ConvertToChecked.java   | 54 ++++++++++++++++++----
 .../calcite/sql2rel/StandardConvertletTable.java   |  2 +-
 core/src/test/resources/sql/scalar.iq              | 26 +++++++++++
 5 files changed, 100 insertions(+), 15 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/prepare/Prepare.java 
b/core/src/main/java/org/apache/calcite/prepare/Prepare.java
index 586cce8385..1c6d8875cb 100644
--- a/core/src/main/java/org/apache/calcite/prepare/Prepare.java
+++ b/core/src/main/java/org/apache/calcite/prepare/Prepare.java
@@ -256,11 +256,14 @@ public PreparedResult prepareSql(
 
     RelRoot root =
         sqlToRelConverter.convertQuery(sqlQuery, needsValidation, true);
-    if (this.context.config().conformance().checkedArithmetic()) {
-      ConvertToChecked checkedConv = new 
ConvertToChecked(root.rel.getCluster().getRexBuilder());
-      RelNode rel = checkedConv.visit(root.rel);
-      root = root.withRel(rel);
-    }
+    boolean convertToChecked = 
this.context.config().conformance().checkedArithmetic();
+    // Convert some operations to use checked arithmetic:
+    // - all arithmetic operations on exact types if the conformance requires 
checked arithmetic
+    // - all arithmetic that produces INTERVAL results, regardless of the 
conformance
+    ConvertToChecked checkedConv =
+        new ConvertToChecked(root.rel.getCluster().getRexBuilder(), 
convertToChecked);
+    RelNode rel = checkedConv.visit(root.rel);
+    root = root.withRel(rel);
     Hook.CONVERTED.run(root.rel);
 
     if (timingTracer != null) {
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index cc6b56520d..c1ebafe8ed 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -2976,10 +2976,14 @@ public static long checkedDivide(long b0, long b1) {
     if ((b0 & b1 & q) >= 0) {
       return q;
     } else {
-      throw new ArithmeticException("integer overflow");
+      throw new ArithmeticException("long overflow");
     }
   }
 
+  public static double checkedDivide(int b0, double b1) {
+    return b0 / b1;
+  }
+
   public static UByte checkedDivide(UByte b0, UByte b1) {
     return UByte.valueOf(b0.intValue() / b1.intValue());
   }
@@ -2996,6 +3000,16 @@ public static ULong checkedDivide(ULong b0, ULong b1) {
     return 
ULong.valueOf(UnsignedType.toBigInteger(b0).divide(UnsignedType.toBigInteger(b1)));
   }
 
+  // The definition of this function must match the divide function with the 
same signature
+  public static int checkedDivide(int b0, BigDecimal b1) {
+    return BigDecimal.valueOf(b0)
+        .divide(b1, RoundingMode.HALF_DOWN).intValueExact();
+  }
+
+  public static BigDecimal checkedDivide(BigDecimal b0, BigDecimal b1) {
+    return b0.divide(b1, RoundingMode.HALF_DOWN);
+  }
+
   // *
 
   /** SQL <code>*</code> operator applied to int values. */
@@ -3118,6 +3132,10 @@ public static ULong checkedMultiply(ULong b0, ULong b1) {
     return 
ULong.valueOf(UnsignedType.toBigInteger(b0).multiply(UnsignedType.toBigInteger(b1)));
   }
 
+  public static BigDecimal checkedMultiply(BigDecimal b0, long b1) {
+    return b0.multiply(BigDecimal.valueOf(b1));
+  }
+
   /** SQL <code>SAFE_ADD</code> function applied to long values. */
   public static @Nullable Long safeAdd(long b0, long b1) {
     try {
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java 
b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
index a5e658e54a..e8c4697c12 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
@@ -38,8 +38,8 @@
 public class ConvertToChecked extends RelHomogeneousShuttle {
   final ConvertRexToChecked converter;
 
-  public ConvertToChecked(RexBuilder builder) {
-    this.converter = new ConvertRexToChecked(builder);
+  public ConvertToChecked(RexBuilder builder, boolean allArithmetic) {
+    this.converter = new ConvertRexToChecked(builder, allArithmetic);
   }
 
   @Override public RelNode visit(RelNode other) {
@@ -48,14 +48,36 @@ public ConvertToChecked(RexBuilder builder) {
   }
 
   /**
-   * Visitor which rewrites an expression tree such that all
-   * arithmetic operations that produce numeric values use checked arithmetic.
+   * Visitor which rewrites an expression tree such that arithmetic operations
+   * use checked arithmetic.
    */
   class ConvertRexToChecked extends RexShuttle {
     private final RexBuilder builder;
+    // If true all arithmetic operations are converted.
+    // Otherwise, only arithmetic operations on INTERVAL values is checked.
+    private final boolean allArithmetic;
 
+    /**
+     * Create a visitor which converts all arithmetic operations to checked.
+     *
+     * @deprecated Use #ConvertRexToChecked(RexBuilder, boolean).
+     */
+    @Deprecated
     ConvertRexToChecked(RexBuilder builder) {
+      this(builder, true);
+    }
+
+    /**
+     * Create a converter that replaces arithmetic with checked arithmetic.
+     *
+     * @param builder         RexBuilder to use.
+     * @param allArithmetic   If true all exact arithmetic operations are 
converted to checked.
+     *                        If false, only operations that produce 
INTERVAL-typed results
+     *                        are converted to checked.
+     */
+    ConvertRexToChecked(RexBuilder builder, boolean allArithmetic) {
       this.builder = builder;
+      this.allArithmetic = allArithmetic;
     }
 
     @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
@@ -72,6 +94,22 @@ class ConvertRexToChecked extends RexShuttle {
       List<RexNode> clonedOperands = visitList(call.operands, update);
       SqlKind kind = call.getKind();
       SqlOperator operator = call.getOperator();
+      SqlTypeName resultType = call.getType().getSqlTypeName();
+      boolean anyOperandIsInterval = false;
+      for (RexNode op : call.getOperands()) {
+        if 
(SqlTypeName.INTERVAL_TYPES.contains(op.getType().getSqlTypeName())) {
+          anyOperandIsInterval = true;
+          break;
+        }
+      }
+      boolean resultIsInterval = 
SqlTypeName.INTERVAL_TYPES.contains(resultType);
+      boolean rewrite =
+          // Do not rewrite operator if the type is e.g., DOUBLE or DATE
+          (this.allArithmetic && SqlTypeName.EXACT_TYPES.contains(resultType))
+          // But always rewrite if the type is an INTERVAL and any operand is 
INTERVAL
+          // This will not rewrite date subtraction, for example
+          || (resultIsInterval && anyOperandIsInterval);
+
       switch (kind) {
       case PLUS:
         operator = SqlStdOperatorTable.CHECKED_PLUS;
@@ -91,8 +129,7 @@ class ConvertRexToChecked extends RexShuttle {
       default:
         break;
       }
-      SqlTypeName resultType = call.getType().getSqlTypeName();
-      if (resultType == SqlTypeName.DECIMAL) {
+      if (resultType == SqlTypeName.DECIMAL && this.allArithmetic) {
         // Checked decimal arithmetic is implemented using unchecked
         // arithmetic followed by a CAST, which is always checked
         RexCall result;
@@ -102,8 +139,9 @@ class ConvertRexToChecked extends RexShuttle {
           result = call;
         }
         return builder.makeCast(call.getParserPosition(), call.getType(), 
result);
-      } else if (!SqlTypeName.EXACT_TYPES.contains(resultType)) {
-        // Do not rewrite operator if the type is e.g., DOUBLE or DATE
+      }
+
+      if (!rewrite) {
         operator = call.getOperator();
       }
       update[0] = update[0] || operator != call.getOperator();
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java 
b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index e8b1bcbd7a..739c3e30fa 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -552,7 +552,7 @@ private static RexNode convertInterval(SqlRexContext cx, 
SqlCall call) {
         SqlLiteral.createInterval(1, "1", intervalQualifier,
             call.getParserPosition());
     final SqlCall multiply =
-        SqlStdOperatorTable.MULTIPLY.createCall(call.getParserPosition(), n,
+        
SqlStdOperatorTable.CHECKED_MULTIPLY.createCall(call.getParserPosition(), n,
             literal);
     return cx.convertExpression(multiply);
   }
diff --git a/core/src/test/resources/sql/scalar.iq 
b/core/src/test/resources/sql/scalar.iq
index d82d69f589..e4ef19c0d3 100644
--- a/core/src/test/resources/sql/scalar.iq
+++ b/core/src/test/resources/sql/scalar.iq
@@ -18,6 +18,32 @@
 !set outputformat mysql
 !use scott
 
+# 5 test cases for [CALCITE-7443]  Incorrect simplification for large interval
+SELECT -(INTERVAL -2147483648 months);
+java.lang.ArithmeticException: integer overflow
+
+!error
+
+SELECT INTERVAL 2147483647 years;
+java.lang.ArithmeticException: integer overflow
+
+!error
+
+SELECT -(INTERVAL -9223372036854775.808 SECONDS);
+java.lang.ArithmeticException: long overflow
+
+!error
+
+SELECT INTERVAL 3000000 months * 1000;
+java.lang.ArithmeticException: integer overflow
+
+!error
+
+SELECT INTERVAL 3000000 months / .0001;
+java.lang.ArithmeticException: Overflow
+
+!error
+
 select deptno, (select min(empno) from "scott".emp where deptno = dept.deptno) 
as x from "scott".dept;
 +--------+------+
 | DEPTNO | X    |

Reply via email to