This is an automated email from the ASF dual-hosted git repository.
julianhyde 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 4e9460600c [CALCITE-7826] IS NOT DISTINCT FROM gives wrong result in
Enumerable convention when applied to DECIMAL values of different scales
4e9460600c is described below
commit 4e9460600c0437478878bbd71875f517c9e576fc
Author: Julian Hyde <[email protected]>
AuthorDate: Sat Sep 26 13:09:54 2026 -0700
[CALCITE-7826] IS NOT DISTINCT FROM gives wrong result in Enumerable
convention when applied to DECIMAL values of different scales
In the Enumerable convention, IS NOT DISTINCT FROM and IS DISTINCT
FROM are implemented by RexImpTable.DistinctFromImplementor, which
compares non-null operands using Objects.equals and does not harmonize
the operand types. BigDecimal.equals is sensitive to scale, so 1.10
(DECIMAL(3, 2)) and 1.1 (DECIMAL(2, 1)) are distinct.
The problem can only be reproduced by creating a RexCall by hand. If a
query is parsed from SQL,
StandardConvertletTable.convertIsDistinctFrom expands IS NOT DISTINCT
FROM into IS NULL and '=' on operands that have been cast to a common
type. RelBuilder.isNotDistinctFrom uses the same expansion.
To fix the problem, DistinctFromImplementor now compares BigDecimal
values using SqlFunctions.eq(BigDecimal, BigDecimal).
Add BuiltInMethod.EQ_DECIMAL constant to reference the SqlFunctions.eq
method.
Close apache/calcite#5296
---
.../calcite/adapter/enumerable/RexImpTable.java | 20 ++++++++++++--
.../org/apache/calcite/util/BuiltInMethod.java | 1 +
.../org/apache/calcite/test/RelBuilderTest.java | 32 ++++++++++++++++++++++
3 files changed, 51 insertions(+), 2 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 6ef0182b30..28083c6616 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
@@ -5050,11 +5050,27 @@ protected Expression
valueExpression(RexToLixTranslator.Result left,
Expressions.condition(right.isNullVariable, BOXED_TRUE_EXPR,
BOXED_FALSE_EXPR),
Expressions.condition(right.isNullVariable, BOXED_FALSE_EXPR,
Expressions.condition(
- Expressions.call(BuiltInMethod.OBJECTS_EQUAL.method,
- left.valueVariable, right.valueVariable),
+ equalExpression(left.valueVariable, right.valueVariable),
BOXED_TRUE_EXPR, BOXED_FALSE_EXPR)));
}
+ /** Returns an expression that compares two non-null values for
+ * equality.
+ *
+ * <p>{@link BigDecimal#equals} is sensitive to scale (for example,
+ * {@code 1.10} is not equal to {@code 1.1}), so {@code BigDecimal} values
+ * are compared using {@link SqlFunctions#eq(BigDecimal, BigDecimal)}, as
+ * the {@code =} operator compares them. Other values are compared using
+ * {@link java.util.Objects#equals}. */
+ private static Expression equalExpression(Expression left,
+ Expression right) {
+ if (left.getType() == BigDecimal.class
+ && right.getType() == BigDecimal.class) {
+ return Expressions.call(BuiltInMethod.EQ_DECIMAL.method, left, right);
+ }
+ return Expressions.call(BuiltInMethod.OBJECTS_EQUAL.method, left, right);
+ }
+
@Override Expression implementSafe(final RexToLixTranslator translator,
final RexCall call, final List<Expression> argValueList) {
throw new IllegalStateException("This implementSafe should not be
called,"
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index a8c006d61e..788fe26d29 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -719,6 +719,7 @@ public enum BuiltInMethod {
Comparable.class),
LT(SqlFunctions.class, "lt", boolean.class, boolean.class),
GT(SqlFunctions.class, "gt", boolean.class, boolean.class),
+ EQ_DECIMAL(SqlFunctions.class, "eq", BigDecimal.class, BigDecimal.class),
BIT_AND(SqlFunctions.class, "bitAnd", long.class, long.class),
BITCOUNT(SqlFunctions.class, "bitCount", BigDecimal.class),
BIT_OR(SqlFunctions.class, "bitOr", long.class, long.class),
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index ebe8990c1d..a5c571e523 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -4908,6 +4908,38 @@ private static RelBuilder assertSize(RelBuilder b,
}
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7826">[CALCITE-7826]
+ * IS NOT DISTINCT FROM gives wrong result in Enumerable convention when
+ * applied to DECIMAL values of different scales</a>. */
+ @Test void testIsNotDistinctFromDecimal() throws Exception {
+ // Equivalent SQL:
+ // SELECT a, b, a = b AS eq, a IS NOT DISTINCT FROM b AS indf,
+ // a IS DISTINCT FROM b AS idf
+ // FROM (VALUES (1.10, 1.1)) AS t (a, b)
+ // but SqlToRelConverter and RelBuilder.isNotDistinctFrom expand IS [NOT]
+ // DISTINCT FROM, so we call the operators directly. "a" has type
+ // DECIMAL(3, 2) and "b" has type DECIMAL(2, 1).
+ final RelBuilder b = RelBuilder.create(config().build());
+ final RelNode root =
+ b.values(new String[] {"a", "b"},
+ new BigDecimal("1.10"), new BigDecimal("1.1"))
+ .project(b.field("a"), b.field("b"),
+ b.alias(b.equals(b.field("a"), b.field("b")), "eq"),
+ b.alias(
+ b.call(SqlStdOperatorTable.IS_NOT_DISTINCT_FROM,
+ b.field("a"), b.field("b")), "indf"),
+ b.alias(
+ b.call(SqlStdOperatorTable.IS_DISTINCT_FROM,
+ b.field("a"), b.field("b")), "idf"))
+ .build();
+ try (PreparedStatement preparedStatement = RelRunners.run(root)) {
+ String s = CalciteAssert.toString(preparedStatement.executeQuery());
+ final String result = "a=1.10; b=1.1; eq=true; indf=true; idf=false\n";
+ assertThat(s, is(result));
+ }
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-1595">[CALCITE-1595]
* RelBuilder.call throws NullPointerException if argument types are