Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions core/src/main/java/org/apache/calcite/prepare/Prepare.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 19 additions & 1 deletion core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -2971,10 +2971,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());
}
Expand All @@ -2991,6 +2995,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. */
Expand Down Expand Up @@ -3113,6 +3127,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
public class ConvertToChecked extends RelHomogeneousShuttle {
final ConvertRexToChecked converter;

public ConvertToChecked(RexBuilder builder) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an
"@deprecated" annotation here to preserve the original method?

this.converter = new ConvertRexToChecked(builder);
public ConvertToChecked(RexBuilder builder, boolean allArithmetic) {
this.converter = new ConvertRexToChecked(builder, allArithmetic);
}

@Override public RelNode visit(RelNode other) {
Expand All @@ -48,14 +48,25 @@ 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;

ConvertRexToChecked(RexBuilder builder) {
/** 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) {
Expand All @@ -72,6 +83,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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the comments are finished, because there's no further content after "for example".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the example is "it will not rewrite date subtraction"

|| (resultIsInterval && anyOperandIsInterval);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the condition anyOperandIsInterval needed?🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid rewriting date subtraction


switch (kind) {
case PLUS:
operator = SqlStdOperatorTable.CHECKED_PLUS;
Expand All @@ -91,8 +118,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;
Expand All @@ -102,8 +128,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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
26 changes: 26 additions & 0 deletions core/src/test/resources/sql/scalar.iq
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"java.lang.ArithmeticException: INTERVAL MONTH value out of integer range"
Can the error message be made more informative? For example, when encountering an error in a complex SQL, it might be difficult to quickly pinpoint the exact location. Could we even include the specific number in the error message?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a very difficult problem. The Enumerable API has not been designed for ergonomic handling of runtime errors. Normally it would (1) install catch blocks and (2) use source-position information to give more meaningful error messages. This would be out of scope in this PR anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, let's set this aside for now.


!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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this outputs a more complete error message; I'll take a look at the code later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reviewing the code, it seems the error message wasn't thrown by Calcite, but rather by the Java API, which makes sense now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is not informative, it is the innermost exception, which is not very helpful. But this is the best we can do now.


!error

select deptno, (select min(empno) from "scott".emp where deptno = dept.deptno) as x from "scott".dept;
+--------+------+
| DEPTNO | X |
Expand Down
Loading