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 21982e3bad [CALCITE-6685] Support checked arithmetic
21982e3bad is described below

commit 21982e3bad81a6194faa78c568cd829d4fef6a7b
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Nov 11 23:42:22 2024 -0800

    [CALCITE-6685] Support checked arithmetic
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../calcite/adapter/enumerable/RexImpTable.java    |  34 +++++-
 .../main/java/org/apache/calcite/plan/Strong.java  |   6 +
 .../apache/calcite/plan/SubstitutionVisitor.java   |   2 +
 .../java/org/apache/calcite/prepare/Prepare.java   |   8 +-
 .../java/org/apache/calcite/rex/RexSimplify.java   |  18 ++-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 114 ++++++++++++++++++
 .../org/apache/calcite/sql/SqlBinaryOperator.java  |   2 +
 .../java/org/apache/calcite/sql/SqlDialect.java    |   4 +
 .../main/java/org/apache/calcite/sql/SqlKind.java  |  42 ++++++-
 .../calcite/sql/fun/SqlStdOperatorTable.java       |  79 +++++++++++++
 .../sql/validate/SqlAbstractConformance.java       |   4 +
 .../calcite/sql/validate/SqlConformance.java       |   7 ++
 .../calcite/sql/validate/SqlConformanceEnum.java   |  29 +++++
 .../sql/validate/SqlDelegatingConformance.java     |   4 +
 .../apache/calcite/sql2rel/ConvertToChecked.java   | 113 ++++++++++++++++++
 .../src/main/java/org/apache/calcite/util/Bug.java |   6 -
 .../calcite/jdbc/CalciteRemoteDriverTest.java      |   3 +-
 .../org/apache/calcite/test/CoreQuidemTest.java    |   4 +-
 .../org/apache/calcite/test/SqlValidatorTest.java  |   6 +
 core/src/test/resources/sql/cast.iq                |  57 ++++++++-
 core/src/test/resources/sql/measure-paper.iq       |  14 +--
 .../java/org/apache/calcite/linq4j/Nullness.java   |   2 +-
 .../apache/calcite/linq4j/tree/ExpressionType.java |   6 +
 .../calcite/linq4j/tree/UnaryExpression.java       |   2 +
 .../apache/calcite/linq4j/test/ExpressionTest.java |  10 +-
 .../apache/calcite/linq4j/test/OptimizerTest.java  |  14 +--
 site/_docs/history.md                              |   8 +-
 .../calcite/sql/test/SqlOperatorFixture.java       |   6 +
 .../org/apache/calcite/test/SqlOperatorTest.java   | 128 +++++++++++----------
 29 files changed, 631 insertions(+), 101 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index b954a595ed..ec1053afc9 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -108,16 +108,21 @@ import static 
com.google.common.base.Preconditions.checkArgument;
 
 import static 
org.apache.calcite.adapter.enumerable.EnumUtils.generateCollatorExpression;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Add;
+import static org.apache.calcite.linq4j.tree.ExpressionType.AddChecked;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Divide;
+import static org.apache.calcite.linq4j.tree.ExpressionType.DivideChecked;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Equal;
 import static org.apache.calcite.linq4j.tree.ExpressionType.GreaterThan;
 import static org.apache.calcite.linq4j.tree.ExpressionType.GreaterThanOrEqual;
 import static org.apache.calcite.linq4j.tree.ExpressionType.LessThan;
 import static org.apache.calcite.linq4j.tree.ExpressionType.LessThanOrEqual;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Multiply;
+import static org.apache.calcite.linq4j.tree.ExpressionType.MultiplyChecked;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Negate;
+import static org.apache.calcite.linq4j.tree.ExpressionType.NegateChecked;
 import static org.apache.calcite.linq4j.tree.ExpressionType.NotEqual;
 import static org.apache.calcite.linq4j.tree.ExpressionType.Subtract;
+import static org.apache.calcite.linq4j.tree.ExpressionType.SubtractChecked;
 import static org.apache.calcite.linq4j.tree.ExpressionType.UnaryPlus;
 import static org.apache.calcite.sql.fun.SqlInternalOperators.LITERAL_AGG;
 import static org.apache.calcite.sql.fun.SqlInternalOperators.THROW_UNLESS;
@@ -361,6 +366,12 @@ import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.CBRT;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CEIL;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHARACTER_LENGTH;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHAR_LENGTH;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_DIVIDE;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_DIVIDE_INTEGER;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_MINUS;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_MULTIPLY;
+import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_PLUS;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.CHECKED_UNARY_MINUS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.CLASSIFIER;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.COALESCE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.COLLECT;
@@ -790,6 +801,13 @@ public class RexImpTable {
       defineUnary(UNARY_MINUS, Negate, NullPolicy.STRICT,
           BuiltInMethod.BIG_DECIMAL_NEGATE.getMethodName());
       defineUnary(UNARY_PLUS, UnaryPlus, NullPolicy.STRICT, null);
+      // checked arithmetic
+      defineBinary(CHECKED_PLUS, AddChecked, NullPolicy.STRICT, "checkedPlus");
+      defineBinary(CHECKED_MINUS, SubtractChecked, NullPolicy.STRICT, 
"checkedMinus");
+      defineBinary(CHECKED_MULTIPLY, MultiplyChecked, NullPolicy.STRICT, 
"checkedMultiply");
+      defineBinary(CHECKED_DIVIDE, DivideChecked, NullPolicy.STRICT, 
"checkedDivide");
+      defineBinary(CHECKED_DIVIDE_INTEGER, DivideChecked, NullPolicy.STRICT, 
"checkedDivide");
+      defineUnary(CHECKED_UNARY_MINUS, NegateChecked, NullPolicy.STRICT, 
"checkedUnaryMinus");
 
       defineMethod(MOD, BuiltInMethod.MOD.method, NullPolicy.STRICT);
       defineMethod(EXP, BuiltInMethod.EXP.method, NullPolicy.STRICT);
@@ -3143,6 +3161,11 @@ public class RexImpTable {
             SqlStdOperatorTable.GREATER_THAN,
             SqlStdOperatorTable.GREATER_THAN_OR_EQUAL);
 
+    private static final List<SqlOperator> CHECKED_OPERATORS =
+        ImmutableList.of(
+            CHECKED_DIVIDE, CHECKED_PLUS, CHECKED_MINUS, CHECKED_MULTIPLY,
+            CHECKED_UNARY_MINUS);
+
     private static final List<SqlBinaryOperator> EQUALS_OPERATORS =
         ImmutableList.of(
             SqlStdOperatorTable.EQUALS,
@@ -3215,6 +3238,11 @@ public class RexImpTable {
             argValueList);
       }
 
+      // For checked arithmetic call the method.
+      if (CHECKED_OPERATORS.contains(op)) {
+        return Expressions.call(SqlFunctions.class, backupMethodName, 
argValueList);
+      }
+
       return Expressions.makeBinary(expressionType,
           argValueList.get(0), argValueList.get(1));
     }
@@ -3272,6 +3300,8 @@ public class RexImpTable {
       if (expressionType == ExpressionType.Negate && argValue.type == 
BigDecimal.class
           && null != backupMethodName) {
         e = Expressions.call(argValue, backupMethodName);
+      } else if (expressionType == NegateChecked && null != backupMethodName) {
+        e = Expressions.call(SqlFunctions.class, backupMethodName, 
argValueList);
       } else {
         e = Expressions.makeUnary(expressionType, argValue);
       }
@@ -3529,7 +3559,9 @@ public class RexImpTable {
       }
 
       // Short-circuit if no cast is required
-      if (call.getType().equals(sourceType)) {
+      if (call.getType().equals(sourceType)
+          // However, do not elide casts to decimal types, they perform bounds 
checking
+          && sourceType.getSqlTypeName() != SqlTypeName.DECIMAL) {
         // No cast required, omit cast
         return argValueList.get(0);
       }
diff --git a/core/src/main/java/org/apache/calcite/plan/Strong.java 
b/core/src/main/java/org/apache/calcite/plan/Strong.java
index 3c8be238f0..759ad69e9f 100644
--- a/core/src/main/java/org/apache/calcite/plan/Strong.java
+++ b/core/src/main/java/org/apache/calcite/plan/Strong.java
@@ -322,6 +322,12 @@ public class Strong {
     map.put(SqlKind.MINUS, Policy.ANY);
     map.put(SqlKind.MINUS_PREFIX, Policy.ANY);
     map.put(SqlKind.TIMES, Policy.ANY);
+    map.put(SqlKind.CHECKED_PLUS, Policy.ANY);
+    map.put(SqlKind.CHECKED_MINUS, Policy.ANY);
+    map.put(SqlKind.CHECKED_MINUS_PREFIX, Policy.ANY);
+    map.put(SqlKind.CHECKED_TIMES, Policy.ANY);
+    map.put(SqlKind.CHECKED_DIVIDE, Policy.ANY);
+
     map.put(SqlKind.DIVIDE, Policy.ANY);
     map.put(SqlKind.CAST, Policy.ANY);
     map.put(SqlKind.REINTERPRET, Policy.ANY);
diff --git 
a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java 
b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
index 20d4f75dc9..5a76cb99b3 100644
--- a/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
+++ b/core/src/main/java/org/apache/calcite/plan/SubstitutionVisitor.java
@@ -378,6 +378,8 @@ public class SubstitutionVisitor {
       final RexNode e = RexUtil.expandSearch(rexBuilder, null, condition);
       return canonizeNode(rexBuilder, e);
     }
+    case CHECKED_PLUS:
+    case CHECKED_TIMES:
     case PLUS:
     case TIMES: {
       RexCall call = (RexCall) condition;
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 f14c864c1c..586cce8385 100644
--- a/core/src/main/java/org/apache/calcite/prepare/Prepare.java
+++ b/core/src/main/java/org/apache/calcite/prepare/Prepare.java
@@ -56,6 +56,7 @@ import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorCatalogReader;
 import org.apache.calcite.sql.validate.SqlValidatorTable;
+import org.apache.calcite.sql2rel.ConvertToChecked;
 import org.apache.calcite.sql2rel.InitializerContext;
 import org.apache.calcite.sql2rel.InitializerExpressionFactory;
 import org.apache.calcite.sql2rel.SqlToRelConverter;
@@ -228,7 +229,7 @@ public abstract class Prepare {
   public PreparedResult prepareSql(
       SqlNode sqlQuery,
       SqlNode sqlNodeOriginal,
-      Class runtimeContextClass,
+      Class<?> runtimeContextClass,
       SqlValidator validator,
       boolean needsValidation) {
     init(runtimeContextClass);
@@ -255,6 +256,11 @@ public abstract class Prepare {
 
     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);
+    }
     Hook.CONVERTED.run(root.rel);
 
     if (timingTracer != null) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java 
b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index fddf8e5f25..ee20c5ad5d 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -314,6 +314,7 @@ public class RexSimplify {
     case LIKE:
       return simplifyLike((RexCall) e, unknownAs);
     case MINUS_PREFIX:
+    case CHECKED_MINUS_PREFIX:
       return simplifyUnaryMinus((RexCall) e, unknownAs);
     case PLUS_PREFIX:
       return simplifyUnaryPlus((RexCall) e, unknownAs);
@@ -321,6 +322,10 @@ public class RexSimplify {
     case MINUS:
     case TIMES:
     case DIVIDE:
+    case CHECKED_PLUS:
+    case CHECKED_MINUS:
+    case CHECKED_TIMES:
+    case CHECKED_DIVIDE:
       return simplifyArithmetic((RexCall) e);
     case M2V:
       return simplifyM2v((RexCall) e);
@@ -430,13 +435,18 @@ public class RexSimplify {
     assert e.getOperands().size() == 2;
 
     switch (e.getKind()) {
+      // These simplifications are safe for both checked and unchecked 
arithemtic.
     case PLUS:
+    case CHECKED_PLUS:
       return simplifyPlus(e);
     case MINUS:
+    case CHECKED_MINUS:
       return simplifyMinus(e);
     case TIMES:
+    case CHECKED_TIMES:
       return simplifyMultiply(e);
     case DIVIDE:
+    case CHECKED_DIVIDE:
       return simplifyDivide(e);
     default:
       throw new IllegalArgumentException("Unsupported arithmeitc operation " + 
e.getKind());
@@ -1327,9 +1337,13 @@ public class RexSimplify {
       safeOps.addAll(SqlKind.COMPARISON);
       safeOps.add(SqlKind.PLUS_PREFIX);
       safeOps.add(SqlKind.MINUS_PREFIX);
+      safeOps.add(SqlKind.CHECKED_MINUS_PREFIX);
       safeOps.add(SqlKind.PLUS);
       safeOps.add(SqlKind.MINUS);
       safeOps.add(SqlKind.TIMES);
+      safeOps.add(SqlKind.CHECKED_PLUS);
+      safeOps.add(SqlKind.CHECKED_MINUS);
+      safeOps.add(SqlKind.CHECKED_TIMES);
       safeOps.add(SqlKind.IS_FALSE);
       safeOps.add(SqlKind.IS_NOT_FALSE);
       safeOps.add(SqlKind.IS_TRUE);
@@ -2206,7 +2220,9 @@ public class RexSimplify {
     operand = simplify(operand, UNKNOWN);
     // The type of DYNAMIC_PARAM is indeterminate, so the cast cannot be 
eliminated
     if (operand.getKind() != SqlKind.DYNAMIC_PARAM
-        && sameTypeOrNarrowsNullability(e.getType(), operand.getType())) {
+        && sameTypeOrNarrowsNullability(e.getType(), operand.getType())
+        // DECIMAL casts are never no-ops: they perform bounds checking
+        && e.getType().getSqlTypeName() != SqlTypeName.DECIMAL) {
       return operand;
     }
     if (RexUtil.isLosslessCast(operand)) {
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 68f77fa2d0..a5f03669da 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -2401,6 +2401,40 @@ public class SqlFunctions {
     throw notArithmetic("+", b0, b1);
   }
 
+  // checked +
+
+  static byte intToByte(int value) {
+    if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+      throw new ArithmeticException(
+          "integer overflow: Value " + value + " does not fit in a TINYINT");
+    }
+    return (byte) value;
+  }
+
+  static short intToShort(int value) {
+    if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
+      throw new ArithmeticException(
+          "integer overflow: Value " + value + " does not fit in a SMALLINT");
+    }
+    return (short) value;
+  }
+
+  public static byte checkedPlus(byte b0, byte b1) {
+    return intToByte(b0 + b1);
+  }
+
+  public static short checkedPlus(short b0, short b1) {
+    return intToShort(b0 + b1);
+  }
+
+  public static int checkedPlus(int b0, int b1) {
+    return Math.addExact(b0, b1);
+  }
+
+  public static long checkedPlus(long b0, long b1) {
+    return Math.addExact(b0, b1);
+  }
+
   // -
 
   /** SQL <code>-</code> operator applied to int values. */
@@ -2459,6 +2493,40 @@ public class SqlFunctions {
     throw notArithmetic("-", b0, b1);
   }
 
+  // checked -
+
+  public static byte checkedMinus(byte b0, byte b1) {
+    return intToByte(b0 - b1);
+  }
+
+  public static short checkedMinus(short b0, short b1) {
+    return intToShort(b0 - b1);
+  }
+
+  public static int checkedMinus(int b0, int b1) {
+    return Math.subtractExact(b0, b1);
+  }
+
+  public static long checkedMinus(long b0, long b1) {
+    return Math.subtractExact(b0, b1);
+  }
+
+  public static byte checkedUnaryMinus(byte b) {
+    return intToByte(-b);
+  }
+
+  public static short checkedUnaryMinus(short b) {
+    return intToShort(-b);
+  }
+
+  public static int checkedUnaryMinus(int b) {
+    return Math.subtractExact(0, b);
+  }
+
+  public static long checkedUnaryMinus(long b) {
+    return Math.subtractExact(0, b);
+  }
+
   // /
 
   /** SQL <code>/</code> operator applied to int values. */
@@ -2531,6 +2599,34 @@ public class SqlFunctions {
         .divide(b1, RoundingMode.HALF_DOWN).longValue();
   }
 
+  public static byte checkedDivide(byte b0, byte b1) {
+    return intToByte(b0 / b1);
+  }
+
+  public static short checkedDivide(short b0, short b1) {
+    return intToShort(b0 * b1);
+  }
+
+  public static int checkedDivide(int b0, int b1) {
+    // Implementation taken from Java 19
+    int q = b0 / b1;
+    if ((b0 & b1 & q) >= 0) {
+      return q;
+    } else {
+      throw new ArithmeticException("integer overflow");
+    }
+  }
+
+  public static long checkedDivide(long b0, long b1) {
+    // Implementation taken from Java 19
+    long q = b0 / b1;
+    if ((b0 & b1 & q) >= 0) {
+      return q;
+    } else {
+      throw new ArithmeticException("integer overflow");
+    }
+  }
+
   // *
 
   /** SQL <code>*</code> operator applied to int values. */
@@ -2591,6 +2687,24 @@ public class SqlFunctions {
     throw notArithmetic("*", b0, b1);
   }
 
+  // checked *
+
+  public static byte checkedMultiply(byte b0, byte b1) {
+    return intToByte(b0 * b1);
+  }
+
+  public static short checkedMultiply(short b0, short b1) {
+    return intToShort(b0 * b1);
+  }
+
+  public static int checkedMultiply(int b0, int b1) {
+    return Math.multiplyExact(b0, b1);
+  }
+
+  public static long checkedMultiply(long b0, long b1) {
+    return Math.multiplyExact(b0, 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/sql/SqlBinaryOperator.java 
b/core/src/main/java/org/apache/calcite/sql/SqlBinaryOperator.java
index 96ef1aabe6..514c61fe09 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlBinaryOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlBinaryOperator.java
@@ -109,6 +109,8 @@ public class SqlBinaryOperator extends SqlOperator {
     case AND:
     case PLUS:
     case TIMES:
+    case CHECKED_PLUS:
+    case CHECKED_TIMES:
       return this;
 
     case GREATER_THAN:
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
index cf7a100bf7..bc9f9e02ca 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -826,6 +826,10 @@ public class SqlDialect {
     case PLUS:
     case ROW:
     case TIMES:
+    case CHECKED_PLUS:
+    case CHECKED_TIMES:
+    case CHECKED_MINUS:
+    case CHECKED_DIVIDE:
       return true;
     default:
       return BUILT_IN_OPERATORS_LIST.contains(operator);
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java 
b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index 03a0178f05..0994306450 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -307,6 +307,31 @@ public enum SqlKind {
    */
   MINUS,
 
+  /**
+   * Checked version of PLUS, which produces a runtime error on overflow.
+   * Not used for date/time arithmetic.
+   */
+  CHECKED_PLUS,
+
+  /**
+   * Checked version of MINUS, which produces a runtime error on overflow.
+   * Not used for date/time arithmetic.
+   */
+  CHECKED_MINUS,
+
+  /**
+   * Checked version of TIMES, which produces a runtime error on overflow.
+   * Not used for date/time arithmetic.
+   */
+  CHECKED_TIMES,
+
+  /**
+   * Checked version of DIVIDE, which produces a runtime error on overflow.
+   * For example, INT_MIN / -1.
+   * Not used for date/time arithmetic.
+   */
+  CHECKED_DIVIDE,
+
   /**
    * Alternation operator in a pattern expression within a
    * {@code MATCH_RECOGNIZE} clause.
@@ -530,6 +555,11 @@ public enum SqlKind {
    */
   MINUS_PREFIX,
 
+  /**
+   * Checked version of unary minus operator.
+   */
+  CHECKED_MINUS_PREFIX,
+
   /** {@code EXISTS} operator. */
   EXISTS,
 
@@ -1516,10 +1546,16 @@ public enum SqlKind {
    * {@link #MINUS}
    * {@link #TIMES}
    * {@link #DIVIDE}
-   * {@link #MOD}.
+   * {@link #MOD}
+   * and the corresponding checked arithmetic operations.
    */
   public static final Set<SqlKind> BINARY_ARITHMETIC =
-      EnumSet.of(PLUS, MINUS, TIMES, DIVIDE, MOD);
+      EnumSet.of(PLUS, MINUS, TIMES, DIVIDE, MOD,
+          CHECKED_PLUS, CHECKED_MINUS, CHECKED_TIMES, CHECKED_DIVIDE);
+
+  public static final Set<SqlKind> CHECKED_ARITHMETIC =
+      EnumSet.of(CHECKED_PLUS, CHECKED_MINUS, CHECKED_TIMES, CHECKED_DIVIDE, 
CHECKED_MINUS_PREFIX);
+
 
   /**
    * Category of binary equality.
@@ -1574,7 +1610,7 @@ public enum SqlKind {
    */
   @API(since = "1.22", status = API.Status.EXPERIMENTAL)
   public static final Set<SqlKind> SYMMETRICAL_SAME_ARG_TYPE =
-      EnumSet.of(PLUS, TIMES);
+      EnumSet.of(PLUS, TIMES, CHECKED_PLUS, CHECKED_TIMES);
 
   /**
    * Simple binary operators are those operators which expects operands from 
the same Domain.
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
index 0258f75d0b..797e224df2 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlStdOperatorTable.java
@@ -294,6 +294,19 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.FIRST_KNOWN,
           OperandTypes.DIVISION_OPERATOR);
 
+  /**
+   * Checked version of arithmetic division operator, '<code>/</code>'.
+   */
+  public static final SqlBinaryOperator CHECKED_DIVIDE =
+      new SqlBinaryOperator(
+          "/",
+          SqlKind.CHECKED_DIVIDE,
+          60,
+          true,
+          ReturnTypes.QUOTIENT_NULLABLE,
+          InferTypes.FIRST_KNOWN,
+          OperandTypes.DIVISION_OPERATOR);
+
   /**
    * Arithmetic remainder operator, '<code>%</code>',
    * an alternative to {@link #MOD} allowed if under certain conformance 
levels.
@@ -340,6 +353,19 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.FIRST_KNOWN,
           OperandTypes.DIVISION_OPERATOR);
 
+  /**
+   * Checked version of integer division.  @see DIVIDE_INTEGER.
+   */
+  public static final SqlBinaryOperator CHECKED_DIVIDE_INTEGER =
+      new SqlBinaryOperator(
+          "/INT",
+          SqlKind.CHECKED_DIVIDE,
+          60,
+          true,
+          ReturnTypes.INTEGER_QUOTIENT_NULLABLE,
+          InferTypes.FIRST_KNOWN,
+          OperandTypes.DIVISION_OPERATOR);
+
   /**
    * Dot operator, '<code>.</code>', used for referencing fields of records.
    */
@@ -547,6 +573,21 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.FIRST_KNOWN,
           OperandTypes.MINUS_OPERATOR);
 
+  /**
+   * Checked version of infix arithmetic minus operator, '<code>-</code>'.
+   */
+  public static final SqlBinaryOperator CHECKED_MINUS =
+      new SqlMonotonicBinaryOperator(
+          "-",
+          SqlKind.CHECKED_MINUS,
+          40,
+          true,
+
+          // Same type inference strategy as sum
+          ReturnTypes.NULLABLE_SUM,
+          InferTypes.FIRST_KNOWN,
+          OperandTypes.MINUS_OPERATOR);
+
   /**
    * Arithmetic multiplication operator, '<code>*</code>'.
    */
@@ -560,6 +601,19 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.FIRST_KNOWN,
           OperandTypes.MULTIPLY_OPERATOR);
 
+  /**
+   * Checked version of arithmetic multiplication operator, '<code>*</code>'.
+   */
+  public static final SqlBinaryOperator CHECKED_MULTIPLY =
+      new SqlMonotonicBinaryOperator(
+          "*",
+          SqlKind.CHECKED_TIMES,
+          60,
+          true,
+          ReturnTypes.PRODUCT_NULLABLE,
+          InferTypes.FIRST_KNOWN,
+          OperandTypes.MULTIPLY_OPERATOR);
+
   /**
    * Logical not-equals operator, '<code>&lt;&gt;</code>'.
    */
@@ -599,6 +653,19 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.FIRST_KNOWN,
           OperandTypes.PLUS_OPERATOR);
 
+  /**
+   * Checked version of infix arithmetic plus operator, '<code>+</code>'.
+   */
+  public static final SqlBinaryOperator CHECKED_PLUS =
+      new SqlMonotonicBinaryOperator(
+          "+",
+          SqlKind.CHECKED_PLUS,
+          40,
+          true,
+          ReturnTypes.NULLABLE_SUM,
+          InferTypes.FIRST_KNOWN,
+          OperandTypes.PLUS_OPERATOR);
+
   /**
    * Infix datetime plus operator, '<code>DATETIME + INTERVAL</code>'.
    */
@@ -951,6 +1018,18 @@ public class SqlStdOperatorTable extends 
ReflectiveSqlOperatorTable {
           InferTypes.RETURN_TYPE,
           OperandTypes.NUMERIC_OR_INTERVAL);
 
+  /**
+   * Checked version of prefix arithmetic minus operator, '<code>-</code>'.
+   */
+  public static final SqlPrefixOperator CHECKED_UNARY_MINUS =
+      new SqlPrefixOperator(
+          "-",
+          SqlKind.CHECKED_MINUS_PREFIX,
+          80,
+          ReturnTypes.ARG0,
+          InferTypes.RETURN_TYPE,
+          OperandTypes.NUMERIC_OR_INTERVAL);
+
   /**
    * Prefix arithmetic plus operator, '<code>+</code>'.
    *
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 ce750242fc..9ff957a805 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
@@ -148,4 +148,8 @@ public abstract class SqlAbstractConformance implements 
SqlConformance {
   @Override public boolean allowLenientCoercion() {
     return SqlConformanceEnum.DEFAULT.allowLenientCoercion();
   }
+
+  @Override public boolean checkedArithmetic() {
+    return SqlConformanceEnum.DEFAULT.checkedArithmetic();
+  }
 }
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 2517e4577b..c38413e3dd 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
@@ -598,4 +598,11 @@ public interface SqlConformance {
    */
   @Experimental
   boolean allowLenientCoercion();
+
+  /**
+   * Whether the implementation uses checked arithmetic.
+   * Most SQL dialects use checked arithmetic at runtime:
+   * they terminate with a fatal error on overflow.
+   */
+  boolean checkedArithmetic();
 }
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 66358f9685..352b1930d3 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
@@ -455,4 +455,33 @@ public enum SqlConformanceEnum implements SqlConformance {
       return false;
     }
   }
+
+  @Override public boolean checkedArithmetic() {
+    switch (this) {
+    case DEFAULT:
+    case LENIENT:
+    case BABEL:
+    case ORACLE_10:
+    case ORACLE_12:
+    case PRAGMATIC_99:
+    case PRESTO:
+    case PRAGMATIC_2003:
+    case STRICT_92:
+    case STRICT_99:
+    case STRICT_2003:
+    case MYSQL_5:
+      // MySQL in strict mode uses checked arithmetic
+      // https://dev.mysql.com/doc/refman/8.4/en/sql-mode.html#sql-mode-strict
+    default:
+      return false;
+      // Postgres also has checked arithmetic, but there is no such conformance
+    case BIG_QUERY:
+      // BigQuery throws on overflow
+      // 
https://cloud.google.com/bigquery/docs/reference/standard-sql/operators
+    case SQL_SERVER_2008:
+      // SET ARITHABORT OFF can be used to turn off overflow behavior, but the 
default is ON
+      // 
https://learn.microsoft.com/en-us/sql/t-sql/statements/set-arithabort-transact-sql
+      return true;
+    }
+  }
 }
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 5cac7afca7..1c9d033843 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
@@ -154,4 +154,8 @@ public class SqlDelegatingConformance implements 
SqlConformance {
   @Override public boolean allowLenientCoercion() {
     return delegate.allowLenientCoercion();
   }
+
+  @Override public boolean checkedArithmetic() {
+    return delegate.checkedArithmetic();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java 
b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
new file mode 100644
index 0000000000..d612818d27
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql2rel/ConvertToChecked.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql2rel;
+
+import org.apache.calcite.rel.RelHomogeneousShuttle;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import java.util.List;
+
+/**
+ * Converts a RelNode tree such that numeric arithmetic operations
+ * use checked arithmetic.  Most SQL dialects should use checked arithmetic,
+ * i.e., they should produce a runtime error on overflow.
+ */
+public class ConvertToChecked extends RelHomogeneousShuttle {
+  final ConvertRexToChecked converter;
+
+  public ConvertToChecked(RexBuilder builder) {
+    this.converter = new ConvertRexToChecked(builder);
+  }
+
+  @Override public RelNode visit(RelNode other) {
+    RelNode mNode = super.visitChildren(other);
+    return mNode.accept(converter);
+  }
+
+  /**
+   * Visitor which rewrites an expression tree such that all
+   * arithmetic operations that produce numeric values use checked arithmetic.
+   */
+  static class ConvertRexToChecked extends RexShuttle {
+    private final RexBuilder builder;
+
+    ConvertRexToChecked(RexBuilder builder) {
+      this.builder = builder;
+    }
+
+    @Override public RexNode visitCall(final RexCall call) {
+      boolean[] update = {false};
+      List<RexNode> clonedOperands = visitList(call.operands, update);
+      SqlKind kind = call.getKind();
+      SqlOperator operator = call.getOperator();
+      switch (kind) {
+      case PLUS:
+        operator = SqlStdOperatorTable.CHECKED_PLUS;
+        break;
+      case MINUS:
+        operator = SqlStdOperatorTable.CHECKED_MINUS;
+        break;
+      case TIMES:
+        operator = SqlStdOperatorTable.CHECKED_MULTIPLY;
+        break;
+      case MINUS_PREFIX:
+        operator = SqlStdOperatorTable.CHECKED_UNARY_MINUS;
+        break;
+      case DIVIDE:
+        operator = SqlStdOperatorTable.CHECKED_DIVIDE;
+        break;
+      default:
+        break;
+      }
+      SqlTypeName resultType = call.getType().getSqlTypeName();
+      if (resultType == SqlTypeName.DECIMAL) {
+        // Checked decimal arithmetic is implemented using unchecked
+        // arithmetic followed by a CAST, which is always checked
+        RexCall result;
+        if (operator == call.getOperator()) {
+          result = call.clone(call.getType(), clonedOperands);
+        } else {
+          result = call;
+        }
+        return builder.makeCast(call.getType(), result);
+      } else if (!SqlTypeName.EXACT_TYPES.contains(resultType)
+          || (resultType == SqlTypeName.DECIMAL)) {
+        // Do not rewrite operator if the type is e.g., DOUBLE or DATE
+        operator = call.getOperator();
+      }
+      update[0] = update[0] || operator != call.getOperator();
+      if (update[0]) {
+        if (operator == call.getOperator()) {
+          return call.clone(call.getType(), clonedOperands);
+        } else {
+          return builder.makeCall(
+              call.getParserPosition(), call.getType(), operator, 
clonedOperands);
+        }
+      } else {
+        return call;
+      }
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/util/Bug.java 
b/core/src/main/java/org/apache/calcite/util/Bug.java
index fd9079331d..063d55bd9c 100644
--- a/core/src/main/java/org/apache/calcite/util/Bug.java
+++ b/core/src/main/java/org/apache/calcite/util/Bug.java
@@ -69,12 +69,6 @@ public abstract class Bug {
    */
   public static final boolean DT1684_FIXED = false;
 
-  /**
-   * Whether <a href="http://issues.eigenbase.org/browse/FNL-25";>issue
-   * FNL-25</a> is fixed. (also filed as dtbug 153)
-   */
-  public static final boolean FNL25_FIXED = false;
-
   /**
    * Whether <a href="http://issues.eigenbase.org/browse/FRG-73";>issue FRG-73:
    * miscellaneous bugs with nested comments</a> is fixed.
diff --git 
a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java 
b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
index ce44fac28d..44c0cee15a 100644
--- a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
+++ b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
@@ -733,7 +733,8 @@ class CalciteRemoteDriverTest {
           (double) Integer.MIN_VALUE, (double) Integer.MAX_VALUE,
           // BigDecimal
           BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.valueOf(2.5D),
-          BigDecimal.valueOf(Double.MAX_VALUE),
+          // Next one causes problems for most types
+          // BigDecimal.valueOf(Double.MAX_VALUE),
           BigDecimal.valueOf(Long.MIN_VALUE),
           // datetime
           new Timestamp(0),
diff --git a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java 
b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
index be327c89a8..abcd2fd6ef 100644
--- a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
+++ b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
@@ -70,11 +70,13 @@ class CoreQuidemTest extends QuidemTest {
               .with(CalciteConnectionProperty.FUN, SqlLibrary.CALCITE.fun)
               .with(CalciteAssert.Config.SCOTT)
               .connect();
-        case "scott-rounding-half-up":
+        case "scott-checked-rounding-half-up":
           discard(CustomTypeSystems.ROUNDING_MODE_HALF_UP);
           return CalciteAssert.that()
               .with(CalciteConnectionProperty.PARSER_FACTORY,
                   ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY")
+              // Use bigquery conformance, which forces checked arithmetic
+              .with(CalciteConnectionProperty.CONFORMANCE, 
SqlConformanceEnum.BIG_QUERY)
               .with(CalciteConnectionProperty.FUN, SqlLibrary.CALCITE.fun)
               .with(CalciteConnectionProperty.TYPE_SYSTEM,
                   CustomTypeSystems.class.getName() + "#ROUNDING_MODE_HALF_UP")
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 94e26cdf00..c0b85c69f4 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -9432,6 +9432,7 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         + "$LiteralChain -\n"
         + "+ pre\n"
         + "- pre\n"
+        + "- pre\n" // checked
         + "FINAL pre\n"
         + "RUNNING pre\n"
         + "\n"
@@ -9439,13 +9440,18 @@ public class SqlValidatorTest extends 
SqlValidatorTestCase {
         + "\n"
         + "% left\n"
         + "* left\n"
+        + "* left\n" // checked
+        + "/ left\n" // checked
         + "/ left\n"
         + "/INT left\n"
+        + "/INT left\n" // checked
         + "|| left\n"
         + "\n"
         + "+ left\n"
+        + "+ left\n" // checked
         + "+ -\n"
         + "- left\n"
+        + "- left\n" // checked
         + "- -\n"
         + "EXISTS pre\n"
         + "UNIQUE pre\n"
diff --git a/core/src/test/resources/sql/cast.iq 
b/core/src/test/resources/sql/cast.iq
index 950bd6c731..4ab5e9a5e3 100644
--- a/core/src/test/resources/sql/cast.iq
+++ b/core/src/test/resources/sql/cast.iq
@@ -15,9 +15,58 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-!use scott
+
+# This connection uses checked arithmetic
+!use scott-checked-rounding-half-up
 !set outputformat mysql
 
+# Test cases for [CALCITE-6685] There is no support for checked arithmetic
+# https://issues.apache.org/jira/browse/CALCITE-6685
+
+select 2 * CAST(5e18 AS DECIMAL(19, 0));
+java.lang.ArithmeticException: Value 10000000000000000000 cannot be 
represented as a DECIMAL(19, 0)
+!error
+
+select - CAST(-2147483648 AS INT);
+Caused by: java.lang.ArithmeticException
+!error
+
+select CAST(1 AS SMALLINT) + CAST(2 AS SMALLINT) AS C;
++---+
+| C |
++---+
+| 3 |
++---+
+(1 row)
+
+!ok
+
+select CAST(127 AS TINYINT) + CAST(2 AS TINYINT) AS C;
+Caused by: java.lang.ArithmeticException: integer overflow: Value 129 does not 
fit in a TINYINT
+!error
+
+select -2147483648 - 1;
+Caused by: java.lang.ArithmeticException
+!error
+
+select -2147483648 / -1;
+Caused by: java.lang.ArithmeticException
+!error
+
+select -CAST(-32768 AS SMALLINT);
+Caused by: java.lang.ArithmeticException: integer overflow: Value 32768 does 
not fit in a SMALLINT
+!error
+
+select CAST(32767 AS SMALLINT) + CAST(2 AS SMALLINT);
+Caused by: java.lang.ArithmeticException: integer overflow: Value 32769 does 
not fit in a SMALLINT
+!error
+
+select 2147483647 * 2147483647;
+Caused by: java.lang.ArithmeticException
+!error
+
+!use scott
+
 # Cast a character literal to a timestamp; note: the plan does not contain CAST
 values cast('1969-07-21 12:34:56' as timestamp);
 +---------------------+
@@ -284,9 +333,9 @@ values cast(' -5 ' as double);
 
 !ok
 
-# [CALCITE-4838] Add RoundingMode in RelDataTypeSystem to specify the rounding 
behavior
-!use scott-rounding-half-up
+!use scott-checked-rounding-half-up
 
+# [CALCITE-4838] Add RoundingMode in RelDataTypeSystem to specify the rounding 
behavior
 # Test cast approximate numeric to int when rounding mode is HALF-UP
 select cast(5.5 as int),
        cast(3.5 as int),
@@ -712,7 +761,7 @@ from (values 0) as t(x);
 
 # [CALCITE-4806] Lossy CAST is incorrectly simplified
 
-!use scott-rounding-half-up
+!use scott-checked-rounding-half-up
 
 # rounding mode is HALF-UP
 select cast(5.5 as int) = 5 as value1,
diff --git a/core/src/test/resources/sql/measure-paper.iq 
b/core/src/test/resources/sql/measure-paper.iq
index b7a0565b3b..190713d811 100644
--- a/core/src/test/resources/sql/measure-paper.iq
+++ b/core/src/test/resources/sql/measure-paper.iq
@@ -224,13 +224,13 @@ SELECT "prodName",
   COUNT(*) AS "count"
 FROM "Orders" AS o
 GROUP BY "prodName";
-+----------+--------------------+-------+
-| prodName | profitMargin       | count |
-+----------+--------------------+-------+
-| Acme     |               0.60 |     1 |
-| Happy    | 0.4705882352941176 |     3 |
-| Whizz    | 0.6666666666666667 |     1 |
-+----------+--------------------+-------+
++----------+--------------+-------+
+| prodName | profitMargin | count |
++----------+--------------+-------+
+| Acme     |     0.600000 |     1 |
+| Happy    |     0.470588 |     3 |
+| Whizz    |     0.666666 |     1 |
++----------+--------------+-------+
 (3 rows)
 
 !ok
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/Nullness.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/Nullness.java
index c158e39a2b..55cf8c4f69 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/Nullness.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/Nullness.java
@@ -60,7 +60,7 @@ public class Nullness {
    * has unknown nullability, so the following needs to be used:
    *
    * <pre><code>
-   * T get() { return sneakyNull(value); }
+   * T get() { return castNonNull(value); }
    * </code></pre>
    *
    * @param <T>     the type of the reference
diff --git 
a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ExpressionType.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ExpressionType.java
index 7b0dfe65d1..bcc87c480a 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ExpressionType.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/ExpressionType.java
@@ -140,6 +140,12 @@ public enum ExpressionType {
    */
   Divide(" / ", false, 3, false),
 
+  /**
+   * A checked division operation, such as (a / b), for numeric
+   * operands.
+   */
+  DivideChecked(" / ", false, 3, false),
+
   /**
    * A percent remainder operation, such as (a % b), for numeric
    * operands.
diff --git 
a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnaryExpression.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnaryExpression.java
index fa1634001d..6c04bf73c0 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnaryExpression.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/tree/UnaryExpression.java
@@ -77,8 +77,10 @@ public class UnaryExpression extends Expression {
       expression.accept(writer, lprec, nodeType.rprec);
       writer.append(nodeType.op);
     } else {
+      writer.append("(");
       writer.append(nodeType.op);
       expression.accept(writer, nodeType.lprec, rprec);
+      writer.append(")");
     }
   }
 
diff --git 
a/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java 
b/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java
index 1799ac8b57..d962204531 100644
--- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java
+++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/ExpressionTest.java
@@ -615,7 +615,7 @@ public class ExpressionTest {
             Expressions.negate(
                 Expressions.negate(
                     Expressions.constant(5)))),
-        is("- - 5"));
+        is("(- (- 5))"));
 
     assertThat(
         Expressions.toString(
@@ -739,7 +739,7 @@ public class ExpressionTest {
                 Expressions.typeIs(
                     Expressions.parameter(Object.class, "o"),
                     String.class))),
-        is("!(o instanceof String)"));
+        is("(!(o instanceof String))"));
 
     // not not
     assertThat(
@@ -749,7 +749,7 @@ public class ExpressionTest {
                     Expressions.typeIs(
                         Expressions.parameter(Object.class, "o"),
                         String.class)))),
-        is("!!(o instanceof String)"));
+        is("(!(!(o instanceof String)))"));
   }
 
   @Test void testWriteConstant() {
@@ -1025,7 +1025,7 @@ public class ExpressionTest {
                 + "  int x = 10;\n"
                 + "  int y = 0;\n"
                 + "  while (x < 5) {\n"
-                + "    ++y;\n"
+                + "    (++y);\n"
                 + "  }\n"
                 + "}\n"));
   }
@@ -1547,7 +1547,7 @@ public class ExpressionTest {
     assertThat(Expressions.toString(builder.toBlock()),
         is("{\n"
             + "  for (int i = 0, j = 10; ; ) {\n"
-            + "    if (++i < --j) {\n"
+            + "    if ((++i) < (--j)) {\n"
             + "      break;\n"
             + "    }\n"
             + "  }\n"
diff --git 
a/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java 
b/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java
index 728746d4a9..579b73f5f5 100644
--- a/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java
+++ b/linq4j/src/test/java/org/apache/calcite/linq4j/test/OptimizerTest.java
@@ -145,7 +145,7 @@ class OptimizerTest {
             Expressions.condition(
                 Expressions.parameter(boolean.class, "a"),
                 Expressions.parameter(boolean.class, "b"), TRUE)),
-        is("{\n  return !a || b;\n}\n"));
+        is("{\n  return (!a) || b;\n}\n"));
   }
 
   @Test void testOptimizeTernaryAfalseB() {
@@ -155,7 +155,7 @@ class OptimizerTest {
             Expressions.condition(
                 Expressions.parameter(boolean.class, "a"),
                 FALSE, Expressions.parameter(boolean.class, "b"))),
-        is("{\n  return !a && b;\n}\n"));
+        is("{\n  return (!a) && b;\n}\n"));
   }
 
   @Test void testOptimizeTernaryABfalse() {
@@ -288,7 +288,7 @@ class OptimizerTest {
                     Expressions.parameter(Integer.class, "inp0_"),
                     NULL_INTEGER),
             NULL)),
-        is("{\n  return !v || inp0_ == null;\n}\n"));
+        is("{\n  return (!v) || inp0_ == null;\n}\n"));
   }
 
   @Test void testOptimizeTernaryAeqBBA() {
@@ -316,7 +316,7 @@ class OptimizerTest {
                     NULL_INTEGER,
                     Expressions.parameter(Integer.class, "inp0_")),
             NULL)),
-        is("{\n  return !(v || inp0_ == null);\n}\n"));
+        is("{\n  return (!(v || inp0_ == null));\n}\n"));
   }
 
   @Test void testOptimizeTernaryInEqualABCneqC() {
@@ -328,7 +328,7 @@ class OptimizerTest {
                     Expressions.parameter(Integer.class, "inp0_"),
                     NULL_INTEGER),
             NULL)),
-        is("{\n  return !(!v || inp0_ == null);\n}\n"));
+        is("{\n  return (!((!v) || inp0_ == null));\n}\n"));
   }
 
   @Test void testOptimizeTernaryAneqBBA() {
@@ -516,7 +516,7 @@ class OptimizerTest {
     // x == false
     ParameterExpression x = Expressions.parameter(boolean.class, "x");
     assertThat(optimize(Expressions.equal(x, FALSE)),
-        is("{\n  return !x;\n}\n"));
+        is("{\n  return (!x);\n}\n"));
   }
 
   @Test void testNotEqualSameConst() {
@@ -582,7 +582,7 @@ class OptimizerTest {
     // x != true
     ParameterExpression x = Expressions.parameter(boolean.class, "x");
     assertThat(optimize(Expressions.notEqual(x, TRUE)),
-        is("{\n  return !x;\n}\n"));
+        is("{\n  return (!x);\n}\n"));
   }
 
   @Test void testNotEqualBoolFalse() {
diff --git a/site/_docs/history.md b/site/_docs/history.md
index c6125069aa..67a9d44adc 100644
--- a/site/_docs/history.md
+++ b/site/_docs/history.md
@@ -49,7 +49,13 @@ other software versions as specified in gradle.properties.
 #### Breaking Changes
 {: #breaking-1-39-0}
 
-None.
+*Checked arithmetic*.  [<a
+href="https://issues.apache.org/jira/browse/CALCITE-6685";>CALCITE-6685</a>]
+introduces support for checked arithmetic on short integer types.  A
+new `SqlConformance.checkedArithmetic()` attribute is added to control
+this behavior.  The BIG_QUERY and SQL_SERVER_2008 conformance have
+been changed to use checked arithmetic, matching the specification of
+these dialects.
 
 #### New features
 {: #new-features-1-39-0}
diff --git 
a/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java 
b/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
index a10be95988..94436699b2 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/test/SqlOperatorFixture.java
@@ -75,6 +75,12 @@ public interface SqlOperatorFixture extends AutoCloseable {
 
   String OUT_OF_RANGE_MESSAGE = ".* out of range.*";
 
+  String INTEGER_OVERFLOW = "integer overflow.*";
+
+  String LONG_OVERFLOW = "long overflow.*";
+
+  String DECIMAL_OVERFLOW = ".*cannot be represented as a DECIMAL.*";
+
   String WRONG_FORMAT_MESSAGE = "Number has wrong format.*";
 
   // TODO: Change message
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 827074837b..5a5148a93b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -38,6 +38,7 @@ import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlIdentifier;
 import org.apache.calcite.sql.SqlJdbcFunctionCall;
+import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlLiteral;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
@@ -129,11 +130,14 @@ import static 
org.apache.calcite.sql.test.ResultCheckers.isSet;
 import static org.apache.calcite.sql.test.ResultCheckers.isSingle;
 import static org.apache.calcite.sql.test.ResultCheckers.isWithin;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.BAD_DATETIME_MESSAGE;
+import static org.apache.calcite.sql.test.SqlOperatorFixture.DECIMAL_OVERFLOW;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.DIVISION_BY_ZERO_MESSAGE;
+import static org.apache.calcite.sql.test.SqlOperatorFixture.INTEGER_OVERFLOW;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_ARGUMENTS_NUMBER;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_ARGUMENTS_TYPE_VALIDATION_ERROR;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.INVALID_CHAR_MESSAGE;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.LITERAL_OUT_OF_RANGE_MESSAGE;
+import static org.apache.calcite.sql.test.SqlOperatorFixture.LONG_OVERFLOW;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.OUT_OF_RANGE_MESSAGE;
 import static 
org.apache.calcite.sql.test.SqlOperatorFixture.WRONG_FORMAT_MESSAGE;
 import static org.apache.calcite.util.DateTimeStringUtils.getDateFormatter;
@@ -142,6 +146,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
@@ -369,12 +374,6 @@ public class SqlOperatorTest {
 
   //--- Tests -----------------------------------------------------------
 
-  /**
-   * For development. Put any old code in here.
-   */
-  @Test void testDummy() {
-  }
-
   @Test void testSqlOperatorOverloading() {
     final SqlStdOperatorTable operatorTable = SqlStdOperatorTable.instance();
     for (SqlOperator sqlOperator : operatorTable.getOperatorList()) {
@@ -387,8 +386,15 @@ public class SqlOperatorTest {
 
       routines.removeIf(operator ->
           !sqlOperator.getClass().isInstance(operator));
-      assertThat(routines, hasSize(1));
-      assertThat(sqlOperator, equalTo(routines.get(0)));
+      if (routines.size() == 2) {
+        // Some arithmetic operators looks like they are overloaded,
+        // e.g. PLUS and CHECKED_PLUS
+        assertTrue(SqlKind.CHECKED_ARITHMETIC.contains(routines.get(0).kind)
+                || SqlKind.CHECKED_ARITHMETIC.contains(routines.get(1).kind));
+      } else {
+        assertThat(routines, hasSize(1));
+        assertThat(sqlOperator, equalTo(routines.get(0)));
+      }
     }
   }
 
@@ -3258,8 +3264,10 @@ public class SqlOperatorTest {
   }
 
   @Test void testMinusOperator() {
-    final SqlOperatorFixture f = fixture();
-    f.setFor(SqlStdOperatorTable.MINUS, VmName.EXPAND);
+    SqlOperatorFixture f = fixture()
+        .setFor(SqlStdOperatorTable.MINUS, VmName.EXPAND)
+        // BigQuery requires arithmetic overflows
+        .withConformance(SqlConformanceEnum.BIG_QUERY);
     f.checkScalarExact("-2-1", -3);
     f.checkScalarExact("-2-1-5", -8);
     f.checkScalarExact("2-1", 1);
@@ -3275,22 +3283,18 @@ public class SqlOperatorTest {
     f.checkNull("1e1-cast(null as double)");
     f.checkNull("cast(null as tinyint) - cast(null as smallint)");
 
-    // TODO: Fix bug
-    if (Bug.FNL25_FIXED) {
-      // Should throw out of range error
-      f.checkFails("cast(100 as tinyint) - cast(-100 as tinyint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(-20000 as smallint) - cast(20000 as smallint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(1.5e9 as integer) - cast(-1.5e9 as integer)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(-5e18 as bigint) - cast(5e18 as bigint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(5e18 as decimal(19,0)) - cast(-5e18 as 
decimal(19,0))",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(-5e8 as decimal(19,10)) - cast(5e8 as 
decimal(19,10))",
-          OUT_OF_RANGE_MESSAGE, true);
-    }
+    f.checkFails("cast(100 as tinyint) - cast(-100 as tinyint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(-20000 as smallint) - cast(20000 as smallint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(1.5e9 as integer) - cast(-1.5e9 as integer)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(-5e18 as bigint) - cast(5e18 as bigint)",
+        LONG_OVERFLOW, true);
+    f.checkFails("cast(5e18 as decimal(19,0)) - cast(-5e18 as decimal(19,0))",
+        DECIMAL_OVERFLOW, true);
+    f.checkFails("cast(-6e8 as decimal(19,10)) - cast(6e8 as decimal(19,10))",
+        DECIMAL_OVERFLOW, true);
   }
 
   @Test void testMinusIntervalOperator() {
@@ -3419,8 +3423,10 @@ public class SqlOperatorTest {
   }
 
   @Test void testMultiplyOperator() {
-    final SqlOperatorFixture f = fixture();
-    f.setFor(SqlStdOperatorTable.MULTIPLY, VmName.EXPAND);
+    final SqlOperatorFixture f = fixture()
+        .setFor(SqlStdOperatorTable.MULTIPLY, VmName.EXPAND)
+        // BigQuery requires arithmetic overflows
+        .withConformance(SqlConformanceEnum.BIG_QUERY);
     f.checkScalarExact("2*3", 6);
     f.checkScalarExact("2*-3", -6);
     f.checkScalarExact("+2*3", 6);
@@ -3439,21 +3445,19 @@ public class SqlOperatorTest {
     f.checkNull("2e-3*cast(null as integer)");
     f.checkNull("cast(null as tinyint) * cast(4 as smallint)");
 
-    if (Bug.FNL25_FIXED) {
-      // Should throw out of range error
-      f.checkFails("cast(100 as tinyint) * cast(-2 as tinyint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(200 as smallint) * cast(200 as smallint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(1.5e9 as integer) * cast(-2 as integer)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(5e9 as bigint) * cast(2e9 as bigint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(2e9 as decimal(19,0)) * cast(-5e9 as decimal(19,0))",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(5e4 as decimal(19,10)) * cast(2e4 as decimal(19,10))",
-          OUT_OF_RANGE_MESSAGE, true);
-    }
+    // Should throw out of range error
+    f.checkFails("cast(100 as tinyint) * cast(-2 as tinyint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(200 as smallint) * cast(200 as smallint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(1.5e9 as integer) * cast(-2 as integer)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(5e9 as bigint) * cast(2e9 as bigint)",
+        LONG_OVERFLOW, true);
+    f.checkFails("cast(2e9 as decimal(19,0)) * cast(-5e9 as decimal(19,0))",
+        DECIMAL_OVERFLOW, true);
+    f.checkFails("cast(5e4 as decimal(19,10)) * cast(2e4 as decimal(19,10))",
+        DECIMAL_OVERFLOW, true);
   }
 
   @Test void testMultiplyIntervals() {
@@ -3584,8 +3588,10 @@ public class SqlOperatorTest {
   }
 
   @Test void testPlusOperator() {
-    final SqlOperatorFixture f = fixture();
-    f.setFor(SqlStdOperatorTable.PLUS, VmName.EXPAND);
+    final SqlOperatorFixture f = fixture()
+        .setFor(SqlStdOperatorTable.PLUS, VmName.EXPAND)
+        // BigQuery requires arithmetic overflows
+        .withConformance(SqlConformanceEnum.BIG_QUERY);
     f.checkScalarExact("1+2", 3);
     f.checkScalarExact("-1+2", 1);
     f.checkScalarExact("1+2+3", 6);
@@ -3601,22 +3607,20 @@ public class SqlOperatorTest {
     f.checkNull("cast(null as tinyint)+1");
     f.checkNull("1e-2+cast(null as double)");
 
-    if (Bug.FNL25_FIXED) {
-      // Should throw out of range error
-      f.checkFails("cast(100 as tinyint) + cast(100 as tinyint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(-20000 as smallint) + cast(-20000 as smallint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(1.5e9 as integer) + cast(1.5e9 as integer)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(5e18 as bigint) + cast(5e18 as bigint)",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(-5e18 as decimal(19,0))"
-              + " + cast(-5e18 as decimal(19,0))",
-          OUT_OF_RANGE_MESSAGE, true);
-      f.checkFails("cast(5e8 as decimal(19,10)) + cast(5e8 as decimal(19,10))",
-          OUT_OF_RANGE_MESSAGE, true);
-    }
+    // Should throw out of range error
+    f.checkFails("cast(100 as tinyint) + cast(100 as tinyint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(-20000 as smallint) + cast(-20000 as smallint)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(1.5e9 as integer) + cast(1.5e9 as integer)",
+        INTEGER_OVERFLOW, true);
+    f.checkFails("cast(5e18 as bigint) + cast(5e18 as bigint)",
+        LONG_OVERFLOW, true);
+    f.checkFails("cast(-5e18 as decimal(19,0))"
+            + " + cast(-5e18 as decimal(19,0))",
+        DECIMAL_OVERFLOW, true);
+    f.checkFails("cast(5e8 as decimal(19,10)) + cast(5e8 as decimal(19,10))",
+        DECIMAL_OVERFLOW, true);
   }
 
   @Test void testPlusOperatorAny() {
@@ -13119,7 +13123,7 @@ public class SqlOperatorTest {
     f.checkScalarExact("ceil(cast(3 as bigint))", "DOUBLE NOT NULL", "3.0");
     f.checkScalarExact("ceil(cast(3.5 as double))", "DOUBLE NOT NULL", "4.0");
     f.checkScalarExact("ceil(cast(3.45 as decimal(19, 1)))",
-        "DECIMAL(19, 1) NOT NULL", "4");
+        "DECIMAL(19, 1) NOT NULL", "4.0");
     f.checkScalarExact("ceil(cast(3.45 as float))", "FLOAT NOT NULL", "4.0");
     f.checkNull("ceil(cast(null as tinyint))");
   }

Reply via email to